Using JSONB_ARRAY_ELEMENTS with WHERE ... IN condition - postgresql

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...

Related

How to pass a parameter and cast to a certain data type inside a function?

I'm attempting to write a plpgsql function to insert a record into a table that expects some timestamps in certain columns. Here is the function:
create or replace function insert_slot(created_by varchar
, version bigint
, bsv_id bigint
, start_date varchar) returns int as $$
declare
last_id int := (select id from appointments order by id desc limit 1) + 1;
begin
insert into appointments (
id,
created,
created_by,
version,
bsv_id,
usrn,
start_date,
end_date,
status,
request_priority_name,
reservation_expiry,
day_of_week
)values (
last_id,
now(),
created_by,
version,
bsv_id,
'UN-' || last_id,
to_timestamp(extract(epoch from timestamp #start_date)),
to_timestamp(extract(epoch from timestamp '2017-2-12 10:30:00')),
'OCCUPIED',
'ROUTINE',
to_timestamp(extract(epoch from timestamp '2017-3-19 10:30:00')),
1
);
return last_id;
end;
$$ LANGUAGE plpgsql;
select * from insert_slot('Brad', 2, 70000, '2017-2-12 10:00:00');
This works fine when I am passing a literal string for the date format, but not for a parameter:
to_timestamp(extract(epoch from timestamp #start_date)),
to_timestamp(extract(epoch from timestamp '2017-2-12 10:30:00')),
When I try to use #start_date or start_date instead of the literal string I get this error:
ERROR: column "timestamp" does not exist
LINE 21: to_timestamp(extract(epoch from timestamp #start_date)),
Can someone enlighten me as to the correct syntax to use? I've found plenty of posts online about using parameters but can't find anything that addresses why I can't pass through the function parameter to epoch from timestamp.
1.
Do not use a # character to prepend variables. That's SQL-Server or MySQL syntax (and maybe others) and illegal in Postgres SQL or PL/pgSQL code.
PL/pgSQL variable names follow the same rules as SQL identifiers. The manual:
SQL identifiers and key words must begin with a letter (a-z, but also
letters with diacritical marks and non-Latin letters) or an underscore
(_). Subsequent characters in an identifier or key word can be
letters, underscores, digits (0-9), or dollar signs ($).
So #start_date is a syntax error.
2.
In this expression:
to_timestamp(extract(epoch from timestamp '2017-2-12 10:30:00')),
timestamp is the data type of the immediately following string literal.
But this notation is not allowed for run-time type conversions. So this is a syntax error:
to_timestamp(extract(epoch from timestamp start_date))
You can use an explicit type cast instead:
to_timestamp(extract(epoch from start_date::timestamp))
The manual:
The ::, CAST(), and function-call syntaxes can also be used to specify
run-time type conversions of arbitrary expressions, as discussed in
Section 4.2.9. To avoid syntactic ambiguity, the type 'string' syntax
can only be used to specify the type of a simple literal constant.
In your particular case it would be smarter / cleaner to define the function parameter as date or timestamp to begin with - depends on what kind of data you plan to pass to the function. Your parameter name indicates a date, but your example indicates a timestamp.
Either way, you don't need to cast that later. EXTRACT() also accepts date and casts it to timestamp automatically.
I would recomend you to use serial/bigserial for id:
CREATE TABLE appointments(id bigserial PRIMARY KEY, ...
Also is better to pass start_date as timestsamp.
create or replace function insert_slot(created_by varchar, version bigint, bsv_id bigint, start_date timestsamp) returns int as $$
For access to function's argument in postgresql you just use its name. Also you can call it via <function_name>.<variable name>. It might be useful when arguments of your function has the same name as column in tables (as is in your function). To avoid this you can add something (for example v_) to names. Also you do not need too complicate construction as to_timestamp(extract(epoch from timestamp '2017-3-19 10:30:00')). Also you can use just sql function.
Improved variant of function:
create table appointments (
id serial PRIMARY KEY,
created timestamp,
created_by text,
version text,
bsv_id int8,
usrn text,
start_date timestamp,
end_date timestamp,
status text,
request_priority_name text,
reservation_expiry timestamp,
day_of_week int2);
CREATE OR REPLACE FUNCTION set_usrn() RETURNS TRIGGER AS $sql$
BEGIN
NEW.usrn := 'UN' || NEW.id;
RETURN NEW;
END;
$sql$ LANGUAGE plpgsql;
CREATE TRIGGER insert_usrn BEFORE INSERT ON appointments FOR EACH ROW EXECUTE PROCEDURE set_usrn();
create or replace function insert_slot(v_created_by varchar, v_version bigint, v_bsv_id bigint, v_start_date timestamp) returns int as $$
insert into appointments (
created,
created_by,
version,
bsv_id,
start_date,
end_date,
status,
request_priority_name,
reservation_expiry,
day_of_week
)values (
now(),
v_created_by,
v_version,
v_bsv_id,
v_start_date,
'2017-2-12 10:30:00',
'OCCUPIED',
'ROUTINE',
'2017-3-19 10:30:00',
1
) RETURNING id;
$$ LANGUAGE sql;
For checking results:
select * from insert_slot('Brad', 2, 70000, '2017-2-12 10:00:00');
SELECT * FROM appointments;
id | created | created_by | version | bsv_id | usrn | start_date | end_date | status | request_priority_name | reservation_expiry | day_of_week
----+----------------------------+------------+---------+--------+------+---------------------+---------------------+----------+-----------------------+---------------------+-------------
1 | 2017-02-05 17:30:59.800305 | Brad | 2 | 70000 | UN1 | 2017-02-12 10:00:00 | 2017-02-12 10:30:00 | OCCUPIED | ROUTINE | 2017-03-19 10:30:00 | 1

can not get all column values separately but instead get all values in one column postgresql --9.5

CREATE OR REPLACE FUNCTION public.get_locations(
location_word varchar(50)
)
RETURNS TABLE
(
country varchar(50),
city varchar(50)
)
AS $$
DECLARE
location_word_ varchar(50);
BEGIN
location_word_:=concat(location_word, '%');
RETURN QUERY EXECUTE format(' (SELECT c.country, ''''::varchar(50) as city FROM webuser.country c
WHERE lower(c.country) LIKE %L LIMIT 1)
UNION
(SELECT c.country,ci.city FROM webuser.country c
JOIN webuser.city ci ON c.country_id=ci.country_id
WHERE lower(ci.city) LIKE %L LIMIT 4)',
location_word_,
location_word_ ) ;
END
$$ language PLPGSQL STABLE;
SELECT public.get_locations('a'::varchar(50));
I get this;
+get_locations +
+record +
------------------
+(Andorra,"") +
+(Germany,Aach) +
+(Germany,Aalen) +
+(Germany,Achim) +
+(Germany,Adenau)+
How can i place/get the values column by column like below? Because otherwise i can not match the values correctly. I should get the values column by column as countries and cities etc.
|country | city |
-------------------------------
| Andorra | "" |
| Germany | Aach |
| Germany | Aalen |
| Germany | Achim |
| Germany | Adenau |
Your function is declared as returns table so you have to use it like a table:
SELECT *
FROM public.get_locations('a'::varchar(50));
Unrelated, but:
Your function is way too complicated, you don't need dynamic SQL, nor do you need a PL/pgSQL function.
You can simplify that to:
CREATE OR REPLACE FUNCTION public.get_locations(p_location_word varchar(50))
RETURNS TABLE(country varchar(50), city varchar(50))
AS $$
(SELECT c.country, ''::varchar(50) as city
FROM webuser.country c
WHERE lower(c.country) LIKE concat(p_location_word, '%')
LIMIT 1)
UNION ALL
(SELECT c.country ,ci.city
FROM webuser.country c
JOIN webuser.city ci ON c.country_id = ci.country_id
WHERE lower(ci.city) LIKE concat(p_location_word, '%')
LIMIT 4)
$$
language SQL;

Prevent empty strings in CHARACTER VARYING field

I am using PostgreSQL and would like to prevent certain required CHARACTER VARYING (VARCHAR) fields from allowing empty string inputs.
These fields would also need to contain unique values, so I am already using a unique constraint; however, this does not prevent an original (unique) empty value.
Basic example, where username needs to be unique and not empty
| id | username | password |
+----+----------+----------+
| 1 | User1 | pw1 | #Allowed
| 2 | User2 | pw1 | #Allowed
| 3 | User2 | pw2 | #Already prevented by constraint
| 4 | '' | pw2 | #Currently allowed, but needs to be prevented
Use a check constraint:
CREATE TABLE foobar(
x TEXT NOT NULL UNIQUE,
CHECK (x <> '')
);
INSERT INTO foobar(x) VALUES('');
You can use the standard SQL 'CONSTRAINT...CHECK' clause when defining table fields:
CREATE TABLE test
(
nonempty VARCHAR NOT NULL UNIQUE CONSTRAINT non_empty CHECK(length(nonempty)>0)
)
As a special kind of constraint, you can put the datatype+constraint into a DOMAIN:
-- set search_path='tmp';
DROP DOMAIN birthdate CASCADE;
CREATE DOMAIN birthdate AS date DEFAULT NULL
CHECK (value >= '1900-01-01' AND value <= now())
;
DROP DOMAIN username CASCADE;
CREATE DOMAIN username AS VARCHAR NOT NULL
CHECK (length(value) > 0)
;
DROP TABLE employee CASCADE;
CREATE TABLE employee
( empno INTEGER NOT NULL PRIMARY KEY
, dob birthdate
, zname username
, UNIQUE (zname)
);
INSERT INTO employee(empno,dob,zname)
VALUES (1,'1980-02-02', 'John Doe' ), (2,'1980-02-02', 'Jon Doeh' );
INSERT INTO employee(empno,dob,zname)
VALUES (3,'1980-02-02', '' ), (4,'1980-01-01', 'Joan Doh' );
This will allow you to reuse the domain again and again, without having to copy the constraint every time.
-- UPDATE 2021-03-25 (Thanks to #AlexanderPavlov)
There appears to be a serious flaw in Postgres's implementation: it is possible to insert NULLs from the results of an empty scalar subquery.
The (nonsensical) COALESCE() below "fixes" this behaviour.
This allows us to put the database into a forbidden state.
\echo literal NULL
INSERT INTO employee(empno,dob,zname) VALUES (5,'2021-02-02', NULL );
\echo empty (scalar) set
INSERT INTO employee(empno,dob,zname) VALUES (6,'2021-02-02', (select zname from employee where 1=0) );
\echo empty COALESCE((scalar, NULL) ) set
INSERT INTO employee(empno,dob,zname) VALUES (7,'2021-02-02', (select COALESCE(zname,NULL) from employee where 1=0) );
\echo empty set#2
INSERT INTO employee(empno,dob,zname) (select 8,'2021-03-03', zname from employee where 1=0 );
\echo duplicate the complete table
INSERT INTO employee(empno,dob,zname) (select 100+empno,dob+'1mon':: interval, upper(zname) from employee );
select * from employee;
Extra Results:
literal NULL
ERROR: domain username does not allow null values
empty (scalar) set
INSERT 0 1
empty COALESCE((scalar, NULL) ) set
ERROR: domain username does not allow null values
empty set#2
INSERT 0 0
duplicate the complete table
ERROR: domain username does not allow null values
empno | dob | zname
-------+------------+----------
1 | 1980-02-02 | John Doe
2 | 1980-02-02 | Jon Doeh
6 | 2021-02-02 |
(3 rows)

Verifying a timestamp is null or in the past

In PostgreSQL 8.4.9 I have a small game, where users can purchase a VIP ("very important person") status:
# \d pref_users;
Table "public.pref_users"
Column | Type | Modifiers
------------+-----------------------------+---------------
id | character varying(32) | not null
vip | timestamp without time zone |
If vip has never been purchased it will be NULL.
If vip has expired it will be < CURRENT_TIMESTAMP.
I'm trying to create PL/pgSQL procedure allowing users with enough vip status left to give a week of it to other users, as a "gift":
create or replace function pref_move_week(_from varchar,
_to varchar) returns void as $BODY$
declare
has_vip boolean;
begin
select vip > current_timestamp + interval '1 week'
into has_vip from pref_users where id=_from;
if (not has_vip) then
return;
end if;
update pref_users set vip = current_timestamp - interval '1 week' where id=_from;
update pref_users set vip = current_timestamp + interval '1 week' where id=_to;
end;
$BODY$ language plpgsql;
Unfortunately this procedure does not work properly if vip of the giving user is NULL:
# update pref_users set vip=null where id='DE16290';
UPDATE 1
# select id,vip from pref_users where id in ('DE16290', 'DE1');
id | vip
---------+----------------------------
DE1 | 2012-01-05 17:35:17.772043
DE16290 |
(2 rows)
# select pref_move_week('DE16290', 'DE1');
pref_move_week
----------------
(1 row)
# select id,vip from pref_users where id in ('DE16290', 'DE1');
id | vip
---------+----------------------------
DE1 | 2012-01-05 17:43:11.589922
DE16290 | 2011-12-22 17:43:11.589922
(2 rows)
I.e. the IF-statement above doesn't seem to work and falls through.
Also I wonder if the has_vip variable is needed here at all?
And how could I ensure that the primary keys _from and _to are really present in the pref_users table or is this already taken care of (because one of the UPDATE statements will "throw an exception" and the transaction will be rollbacked)?
UPDATE:
Thank you for all the replies and I've also got a tip to use:
if (not coalesce(has_vip, false)) then
return;
end if;
But now I have a new problem:
# select id,vip from pref_users where id in ('DE16290', 'DE1');
id | vip
---------+----------------------------
DE1 | 2012-01-05 17:43:11.589922
DE16290 |
(2 rows)
(i.e. DE1 has vip until May and should be able to give a week to DE16290, but):
# select pref_move_week('DE1', 'DE16290');
pref_move_week
----------------
(1 row)
# select id,vip from pref_users where id in ('DE16290', 'DE1');
id | vip
---------+----------------------------
DE1 | 2012-01-05 17:43:11.589922
DE16290 |
(2 rows)
(For some reason nothing has changed?)
UPDATE 2: The final solution -
create or replace function pref_move_week(_from varchar,
_to varchar) returns void as $BODY$
begin
select 1 from pref_users
where id=_from and
vip > current_timestamp + interval '1 week';
if not found then
return;
end if;
update pref_users set
vip = vip - interval '1 week'
where id=_from;
update pref_users set
vip = greatest(vip, current_timestamp) + interval '1 week'
where id=_to;
end;
$BODY$ language plpgsql;
When comparing your timestamp to something that might be null, you can use the COALESE function to supply a "default" value to compare against. For example,
select 1 from pref_users
where id=_from and
vip > COALESCE(current_timestamp, to_timestamp(0));
The COALESCE function returns the first non-null from its list. See docs. (Also to_timestamp() docs...)
Null values return false on any compare short of is null (so where null <> 1 is false and null = 1 is false...null never equals or not equals anything). Instead of
if (not has_vip) then return
go with
if (not has_vip) or has_vip is null then return
The syntax you are using is a bit unique here and I'm not used to reading it...guessing you've come from a different background than SQL and haven't encountered null fun yet. Null is a special concept in SQL that you'll need to handle differently. Remember it is not equal to blank or 0, nor is it less than or greater than any number.
Edit in:
I'm a bit confused by this:
select vip > current_timestamp + interval '1 week'
into has_vip from pref_users where id=_from;
I believe this is equivalent to:
select vip
into has_vip from pref_users
where id=_from
and vip > current_timestamp + interval '1 week';
I hope anyway. Now this is going to return a null if no record is found or if the VIP value is null. can you just go if has_vip is null then return?
There may be a more elegant solution but I think
SELECT (vip IS NOT NULL) AND (vip > current_timestamp + interval '1 week')
would do the work.
I guess your problem is related to the fact that NULL is neither true or false.
You may find more info in the docs.

Calling now() in a function

One of our Postgres tables, called rep_event, has a timestamp column that indicates when each row was inserted. But all of the rows have a timestamp value of 2000-01-01 00:00:00, so something isn't set up right.
There is a function that inserts rows into the table, and it is the only code that inserts rows into that table - no other code inserts into that table. (There also isn't any code that updates the rows in that table.) Here is the definition of the function:
CREATE FUNCTION handle_event() RETURNS "trigger"
AS $$
BEGIN
IF (TG_OP = 'DELETE') THEN
INSERT INTO rep_event SELECT 'D', TG_RELNAME, OLD.object_id, now();
RETURN OLD;
ELSIF (TG_OP = 'UPDATE') THEN
INSERT INTO rep_event SELECT 'U', TG_RELNAME, NEW.object_id, now();
RETURN NEW;
ELSIF (TG_OP = 'INSERT') THEN
INSERT INTO rep_event SELECT 'I', TG_RELNAME, NEW.object_id, now();
RETURN NEW;
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
Here is the table definition:
CREATE TABLE rep_event
(
operation character(1) NOT NULL,
table_name text NOT NULL,
object_id bigint NOT NULL,
time_stamp timestamp without time zone NOT NULL
)
As you can see, the now() function is called to get the current time. Doing a "select now()" on the database returns the correct time, so is there an issue with calling now() from within a function?
A simpler solution is to just modify your table definition to have NOW() be the default value:
CREATE TABLE rep_event (
operation character(1) NOT NULL,
table_name text NOT NULL,
object_id bigint NOT NULL,
time_stamp timestamp without time zone NOT NULL DEFAULT NOW()
);
Then you can get rid of the now() calls in your trigger.
Also as a side note, I strongly suggest including the column ordering in your function... IOW;
INSERT INTO rep_event (operation,table_name,object_id,time_stamp) SELECT ...
This way if you ever add a new column or make other table changes that change the internal ordering of the tables, your function won't suddenly break.
Your problem has to be elsewhere, as your function works well. Create test database, paste the code you cited and run:
create table events (object_id bigserial, data text);
create trigger rep_event
before insert or update or delete on events
for each row execute procedure handle_event();
insert into events (data) values ('v1'),('v2'),('v3');
delete from events where data='v2';
update events set data='v4' where data='v3';
select * from events;
object_id | data
-----------+------
1 | v1
3 | v4
select * from rep_event;
operation | table_name | object_id | time_stamp
-----------+------------+-----------+----------------------------
I | events | 1 | 2011-07-08 10:31:50.489947
I | events | 2 | 2011-07-08 10:31:50.489947
I | events | 3 | 2011-07-08 10:31:50.489947
D | events | 2 | 2011-07-08 10:32:12.65699
U | events | 3 | 2011-07-08 10:32:33.662936
(5 rows)
Check other triggers, trigger creation command etc. And change this timestamp without timezone to timestamp with timezone.