Select and use one column - postgresql

Please consider the following (working) stored procedure. This function receives an integer as its first parameter, indicating which privilegeid has to be checked against the current user. Privileges are stored in the table privileges and consist of an id and a name (varchar).
Each privilege belongs to one or more roles stored in users_roles. Each user is assigned to one or more roles. This function retrieves all roles assigned to current_user and checks them, like said, against the given priviligeid.
CREATE OR REPLACE FUNCTION "public"."has_privilege" (in int4) RETURNS bool AS
$BODY$
DECLARE
asked_privilegeid ALIAS FOR $1;
userid int;
role_row users_roles%rowtype;
privilege_row privileges%rowtype;
BEGIN
EXECUTE 'SELECT userid FROM users WHERE username=$1' INTO userid USING current_user;
FOR role_row IN SELECT * FROM users_roles
WHERE userid=userid
LOOP
IF role_row.roleid = 1 THEN
return TRUE;
END IF;
FOR privilege_row IN SELECT * FROM privileges WHERE roleid=role_row.roleid LOOP
IF privilege_row.privilegeid = asked_privilegeid THEN
return TRUE;
END IF;
END LOOP;
END LOOP;
return FALSE;
END
$BODY$
LANGUAGE 'plpgsql'
However, this code isn't a efficient as it could be considering it retrieves all rowvalues for users_roles and privileges. I tried to write the procedure as following, but it doesn't seem to work:
CREATE OR REPLACE FUNCTION "public"."has_privilege" (in int4) RETURNS bool AS
$BODY$
DECLARE
asked_privilegeid ALIAS FOR $1;
privilegeid int;
userid int;
roleid int;
//role_row users_roles%rowtype;
//privilege_row privileges%rowtype;
BEGIN
EXECUTE 'SELECT userid FROM users WHERE username=$1' INTO userid USING current_user;
FOR roleid IN SELECT roleid FROM users_roles
WHERE userid=userid
LOOP
IF roleid = 1 THEN
return TRUE;
END IF;
FOR privilegeid IN SELECT privilegeid FROM privileges WHERE roleid=roleid LOOP
IF privilegeid = asked_privilegeid THEN
return TRUE;
END IF;
END LOOP;
END LOOP;
return FALSE;
END
$BODY$
LANGUAGE 'plpgsql'
What am I doing wrong? Thanks in advance!
Edit: The indentions didn't came through as expected. Here are pastebin links:
http://pastebin.com/w18WaCW0
http://pastebin.com/W8ewXxEe

The problem is with the row
WHERE userid=userid
It is ambiguous which is the column from the table and which is your variable. Same issue here
FROM privileges WHERE roleid=roleid
Don't use variable names that are also column names that you will reference
You can also rewrite your PROC body as a direct SQL statement that will probably work faster
CREATE OR REPLACE FUNCTION "public"."has_privilege" (in int4) RETURNS bool AS
$BODY$
DECLARE
asked_privilegeid ALIAS FOR $1;
BEGIN
RETURN EXISTS (
SELECT *
FROM users u
INNER JOIN users_roles r on r.userid=u.userid
LEFT JOIN privileges p on p.roleid=r.roleid
AND p.privilegeid = asked_privilegeid
AND r.roleid <> 1 // don't need to process this join if we already have our answer
WHERE u.username = $1
AND (r.roleid=1 OR p.privilegeid is not null))
END
$BODY$
LANGUAGE 'plpgsql'

DECLARE
asked_privilegeid ALIAS FOR $1;
_userid int; -- chage to avoid variable name same as column name
role_row users_roles%rowtype;
privilege_row privileges%rowtype;
BEGIN
_userid := (select userid from users where username = session_user); -- if u use current_user u will have problem when function is defined as security definer
FOR role_row IN SELECT * FROM users_roles WHERE userid = _userid -- your code is error becus userid is same as your variable name
LOOP
IF role_row.roleid = 1 THEN
return TRUE;
END IF;
FOR privilege_row IN SELECT * FROM privileges WHERE roleid=role_row.roleid LOOP
IF privilege_row.privilegeid = asked_privilegeid THEN
return TRUE;
END IF;
END LOOP;
END LOOP;
return FALSE;
END
when u declare variable in postgre, it is good practice that u use underscore b4 variable name '_userid'. to make it distinct from column name

I think this can be solved with just a single SELECT statement:
SELECT count(*)
FROM privileges p
JOIN roles r ON r.privilegeid = p.privilegeid
JOIN user_roles ur ON ur.roleid = r.roleid
JOIN users u ON u.userid = ur.userid AND u.username = session_user
WHERE p.privilegeid = asked_privilegeid
(not tested)
If the count is zero, the privilege is not assigned, otherwise it is.

Related

Postgres query with variable in loop and condition on variable

I have a query which updates the records based on variables old_id and new_id. But condition is I need to fetch the variables dynamically. Here is simple query which I am using.
do
$$
declare
old_id bigint = 1561049391647687270;
declare new_id bigint = 2068236279446765699;
begin
update songs set poet_id = new_id where poet_id = old_id;
update poets set active = true where id = new_id;
update poets set deleted = true where id = old_id;
end
$$;
I need to assign the old_id and new_id dynamically
do
$$
declare
su record;
pc record;
old_id bigint;
new_id bigint;
begin
for pc in select name, count(name)
from poets
where deleted = false
group by name
having count(name) > 1
order by name
loop
for su in select * from poets where name ilike pc.name
loop
-- old_id could be null where I have 2 continue the flow without update
for old_id in (select id from su where su.link is null)
loop
raise notice 'old: %', old_id;
end loop;
-- new_id could be more than 2 skip this condition as well
for new_id in (select id from su where su.link is not null)
loop
raise notice 'new: %', new_id;
end loop;
end loop;
-- run the statement_1 example if new_id and old_id is not null
end loop;
end
$$;
The expected problem statement (to assign variable and use it in further execution) is with in comment.
(a) In your first "simple query", the update of the table poets could be automatically executed by a trigger function defined on the table songs :
CREATE OR REPLACE FUNCTION songs_update_id ()
RETURNS trigger LANGUAGE plpgsql AS
$$
BEGIN
UPDATE poets SET active = true WHERE id = NEW.poet_id ;
UPDATE poets SET deleted = true WHERE id = OLD.poet_id ; -- SET active = false to be added ?
END ;
$$ ;
CREATE OR REPLACE TRIGGER songs_update_id AFTER UPDATE OF id ON songs
FOR EACH ROW EXECUTE songs_update_id () ;
Your first query can then be reduced as :
do
$$
declare
old_id bigint = 1561049391647687270;
declare new_id bigint = 2068236279446765699;
begin
update songs set poet_id = new_id where poet_id = old_id;
end
$$;
(b) The tables update could be performed with a sql query instead of a plpgsql loop and with better performances :
do
$$
BEGIN
UPDATE songs
SET poet_id = list.new_id[1]
FROM
( SELECT b.name
, array_agg(b.id) FILTER (WHERE b.link IS NULL) AS old_id
, array_agg(b.id) FILTER (WHERE b.link IS NOT NULL) AS new_id
FROM
( SELECT name
FROM poets
WHERE deleted = false
GROUP BY name
HAVING COUNT(*) > 1
-- ORDER BY name -- this ORDER BY sounds like useless and resource-intensive
) AS a
INNER JOIN poets AS b
ON b.name ilike a.name
GROUP BY b.name
HAVING array_length(old_id, 1) = 1
AND array_length(new_id, 1) = 1
) AS list
WHERE poet_id = list.old_id[1] ;
END ;
$$;
This solution is not tested yet and could have to be adjusted in order to work correctly. Please provide the tables definition of songs and poets and a sample of data in dbfiddle so that I can test and adjust the proposed solution.

plpgsql return one row or another

I want to look for data in two different ways in a function, and return as soon as I find a result.
First I want to run a query like;
select * from company where company.id = x
Then if that doesn't return results try a query like this
select company.*
from
company
join
company_alias on company.id = company_alias.company_id
where
company_alias.company_alias_id = x;
At the moment I'm doing this with a union all
create or replace function get_payer(x int) returns company as $$
select * from company where company.id = x
union all
select company.*
from
company
join
company_alias on company.id = company_alias.company_id
where
company_alias.company_alias_id = x;
$$ language sql stable
set search_path from current;
This doesn't seem efficient, as I'm always running two queries. But I'm not sure how to structure a condition in a plpgsql function to handle this.
I've tried variations of the following without any luck
create or replace function payment_claim_payer(x int) returns company as $$
declare found_company company;
begin
select * from company where company.id = x into found_company;
if not exists found_company then
select
company.*
from
company
join
company_alias on company.id = company_alias.company_id
where
company_alias.company_alias_id = x into found_company;
end if;
return found_company;
end;
$$ language plpgsql stable
set search_path from current;
Your last attempt was almost hit. You need a plpgsql (not sql) function and should examine the special variable found:
create or replace function payment_claim_payer(x int)
returns company language plpgsql as $$
declare found_company company;
begin
select *
from company
where company.id = x
into found_company;
if not found then
select company.*
from company
join company_alias on company.id = company_alias.company_id
where company_alias.company_alias_id = x
into found_company;
end if;
return found_company;
end;
$$;

Declare row type variable in PL/pgSQL

As I found SELECT * FROM t INTO my_data; works only if:
DO $$
DECLARE
my_data t%ROWTYPE;
BEGIN
SELECT * FROM t INTO my_data WHERE id = ?;
END $$;
Am I right?
If I want to get only 2-3 columns instead of all columns. How can I define my_data?
That is,
DO $$
DECLARE
my_data <WHAT HERE??>;
BEGIN
SELECT id,name,surname FROM t INTO my_data WHERE id = ?;
END $$;
get only 2-3 columns instead of all columns
One way: use a record variable:
DO $$
DECLARE
_rec record;
BEGIN
SELECT INTO _rec
id, name, surname FROM t WHERE id = ?;
END $$;
Note that the structure of a record type is undefined until assigned. So you cannot reference columns (fields) before you do that.
Another way: assign multiple scalar variables:
DO $$
DECLARE
_id int;
_name text;
_surname text;
BEGIN
SELECT INTO _id, _name, _surname
id, name, surname FROM t WHERE id = ?;
END $$;
As for your first example: %ROWTYPE is just noise in Postgres. The documentation:
(Since every table has an associated composite type of the same name,
it actually does not matter in PostgreSQL whether you write %ROWTYPE
or not. But the form with %ROWTYPE is more portable.)
So:
DO $$
DECLARE
my_data t; -- table name serves as type name, too.
BEGIN
SELECT INTO my_data * FROM t WHERE id = ?;
END $$;

PL/pgSQL variable percieved as column

I have a problem with my Postgres and it looks like a simple one. I have done my research but I have not seen anything similar online and would like some clarification:
This is done inside a function, here is the whole code:
BEGIN
IF($5 IS NOT NULL) THEN
BEGIN
INSERT INTO "PushDevice"("DeviceId","PushNotificationId", "pushId","deviceType",sound)
SELECT DISTINCT d.id, $4,d.pushid,d.type,d.sound FROM "Device" d inner join "DeviceLocation" dl ON d.id = dl."DeviceId"
WHERE dl."FIPScode" in (select "FIPScode" from "CountyFIPS" where "stateCode"=$5) AND dl."AppId"=$2 AND d.pushId is not null and d.pushId <>'' and d.pushId<>'1234-5678-9101-2345-3456' and d."isTest"=$3 and d."enableNotification"=TRUE and dl."isDeleted"=0
AND NOT EXISTS (SELECT 1 FROM "PushDevice" t where t."DeviceId"=d.id AND t."PushNotificationId"=$4);
END;
ELSE
DECLARE "epiCentre" VARCHAR := NULL;
magnitude FLOAT = NULL;
BEGIN
SELECT polygon INTO "epiCentre" from alert where id=$1 and "disablePush"=FALSE;
END;
IF("epiCentre" IS NOT NULL) THEN
BEGIN
INSERT INTO "PushDevice"("DeviceId","PushNotificationId", "pushId","deviceType","sound")
SELECT DISTINCT d.id, $4,d."pushId",d.type,d.sound FROM "Device" d inner join "DeviceLocation" dl ON d.id = dl."DeviceId"
WHERE dl."AppId"=$2 AND d."pushId" is not null and d."pushId" <>'' and d."pushId" <>'1234-5678-9101-2345-3456' and d."isTest" =$3 and ST_Distance_Sphere(ST_GeometryFromText("epiCentre"), ST_GeometryFromText(geoPoint))<=d.radius * 1609.344 and magnitude>= d.magnitude and d."enableNotification"=1 and dl."isDeleted"=0
AND NOT EXISTS (SELECT 1 FROM "PushDevice" t where t."DeviceId"=d.id AND t."PushNotificationId"=$4);
END;
END IF;
RETURN QUERY SELECT pd.* FROM "PushDevice" pd
WHERE pd."PushNotificationId" =$4 and pd."sentAt" is null;
END IF;
END;
The problem is here specifically:
DECLARE "epiCentre" VARCHAR := NULL;
magnitude FLOAT = NULL;
BEGIN
SELECT polygon INTO "epiCentre" from alert where id=$1 and "disablePush"=FALSE;
END;
IF("epiCentre" IS NOT NULL) THEN
With error:
Procedure execution failed
ERROR: column "epiCentre" does not exist
LINE 1: SELECT ("epiCentre" IS NOT NULL)
^
QUERY: SELECT ("epiCentre" IS NOT NULL)
CONTEXT: PL/pgSQL function "GetDevicesForPush... line 18 at IF.
So somehow the IF statement perceives epiCentre as column instead of value. And it does not even know it exists although I specifically declared it above.
Any thoughts?
I think you have to many BEGIN-END statements. The declaration of epiCentre is only valid to the first END. And the IF is after that. Therefore I would use on Block for the whole ELSE part.
http://www.postgresql.org/docs/8.3/static/plpgsql-structure.html
As you have found yourself already that DECLARE must be placed before BEGIN of a each block.
More importantly, you do not need multiple blocks here at all. And you don't need a variable either. Use this simpler, safer and faster form:
CREATE function foo(...)
RETURNS ... AS
$func$
BEGIN
IF($5 IS NOT NULL) THEN
-- no redundant BEGIN!
INSERT INTO ... ;
-- and no END!
ELSIF EXISTS (SELECT 1 FROM alert
WHERE id = $1
AND "disablePush" = FALSE
AND polygon IS NOT NULL -- only if polygon can be NULL
) THEN
INSERT INTO ... ;
...
END IF;
END
$func$ LANGUAGE plpgsql;
More Details:
PL/pgSQL checking if a row exists - SELECT INTO boolean

How do you use variables in a simple PostgreSQL script?

For example, in MS-SQL, you can open up a query window and run the following:
DECLARE #List AS VARCHAR(8)
SELECT #List = 'foobar'
SELECT *
FROM dbo.PubLists
WHERE Name = #List
How is this done in PostgreSQL? Can it be done?
Complete answer is located in the official PostgreSQL documentation.
You can use new PG9.0 anonymous code block feature (http://www.postgresql.org/docs/9.1/static/sql-do.html )
DO $$
DECLARE v_List TEXT;
BEGIN
v_List := 'foobar' ;
SELECT *
FROM dbo.PubLists
WHERE Name = v_List;
-- ...
END $$;
Also you can get the last insert id:
DO $$
DECLARE lastid bigint;
BEGIN
INSERT INTO test (name) VALUES ('Test Name')
RETURNING id INTO lastid;
SELECT * FROM test WHERE id = lastid;
END $$;
DO $$
DECLARE
a integer := 10;
b integer := 20;
c integer;
BEGIN
c := a + b;
RAISE NOTICE'Value of c: %', c;
END $$;
You can use:
\set list '''foobar'''
SELECT * FROM dbo.PubLists WHERE name = :list;
That will do
Here's an example of using a variable in plpgsql:
create table test (id int);
insert into test values (1);
insert into test values (2);
insert into test values (3);
create function test_fn() returns int as $$
declare val int := 2;
begin
return (SELECT id FROM test WHERE id = val);
end;
$$ LANGUAGE plpgsql;
SELECT * FROM test_fn();
test_fn
---------
2
Have a look at the plpgsql docs for more information.
I've came across some other documents which they use \set to declare scripting variable but the value is seems to be like constant value and I'm finding for way that can be acts like a variable not a constant variable.
Ex:
\set Comm 150
select sal, sal+:Comm from emp
Here sal is the value that is present in the table 'emp' and comm is the constant value.
Building on #nad2000's answer and #Pavel's answer here, this is where I ended up for my Flyway migration scripts. Handling for scenarios where the database schema was manually modified.
DO $$
BEGIN
IF NOT EXISTS(
SELECT TRUE FROM pg_attribute
WHERE attrelid = (
SELECT c.oid
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE
n.nspname = CURRENT_SCHEMA()
AND c.relname = 'device_ip_lookups'
)
AND attname = 'active_date'
AND NOT attisdropped
AND attnum > 0
)
THEN
RAISE NOTICE 'ADDING COLUMN';
ALTER TABLE device_ip_lookups
ADD COLUMN active_date TIMESTAMP;
ELSE
RAISE NOTICE 'SKIPPING, COLUMN ALREADY EXISTS';
END IF;
END $$;
For use variables in for example alter table:
DO $$
DECLARE name_pk VARCHAR(200);
BEGIN
select constraint_name
from information_schema.table_constraints
where table_schema = 'schema_name'
and table_name = 'table_name'
and constraint_type = 'PRIMARY KEY' INTO name_pk;
IF (name_pk := '') THEN
EXECUTE 'ALTER TABLE schema_name.table_name DROP CONSTRAINT ' || name_pk;
Postgresql does not have bare variables, you could use a temporary table.
variables are only available in code blocks or as a user-interface feature.
If you need a bare variable you could use a temporary table:
CREATE TEMP TABLE list AS VALUES ('foobar');
SELECT dbo.PubLists.*
FROM dbo.PubLists,list
WHERE Name = list.column1;
I had to do something like this
CREATE OR REPLACE FUNCTION MYFUNC()
RETURNS VOID AS $$
DO
$do$
BEGIN
DECLARE
myvar int;
...
END
$do$
$$ LANGUAGE SQL;
You can also simply make a constant query that you use in the actual query:
WITH vars as (SELECT 'foobar' AS list)
SELECT *
FROM dbo.PubLists, vars
WHERE Name = vars.list
Given the popularity, and somewhat incomplete answers I'll provide two solutions.
A do block that won't return rows. You can return rows with a transaction cursor, but it's a bit messy.
A function (that returns rows)
Below I'll use an over-baked example of updating the tweet on the bottom right "blurb" with "hello world".
id (serial)
pub_id (text)
tweet (text)
1
abc
hello world
2
def
blurb
A simple do block
do $$
declare
src_pub_id text;
dst_pub_id text;
src_id int;
dest_id int;
src_tweet text;
begin
src_pub_id := 'abc';
dst_pub_id := 'def';
-- query result into a temp variable
src_id := (select id from tweets where pub_id = src_pub_id);
-- query result into a temp variable (another way)
select tweet into src_tweet from tweets where id = src_id;
dest_id := (select id from tweets where pub_id = dst_pub_id);
update tweets set tweet=src_tweet where id = dest_id;
end $$ language plpgsql; -- need the language to avoid ERROR 42P13
A function
create or replace function sync_tweets(
src_pub_id text, -- function arguments
dst_pub_id text
) returns setof tweets as -- i.e. rows. int, text work too
$$
declare
src_id int; -- temp function variables (not args)
dest_id int;
src_tweet text;
begin
-- query result into a temp variable
src_id := (select id from tweets where pub_id = src_pub_id);
-- query result into a temp variable (another way)
select tweet into src_tweet from tweets where id = src_id;
dest_id := (select id from tweets where pub_id = dst_pub_id);
update tweets set tweet=src_tweet where id = dest_id;
return query -- i.e. rows, return 0 with return int above works too
select * from tweets where pub_id in (src_pub_id, dst_pub_id);
end
$$ language plpgsql; -- need the language to avoid ERROR 42P13
-- Run it!
select * from sync_tweets('abc', 'def');
-- Optional drop if you don't want the db to keep your function
drop function if exists sync_tweets(text, text);
/*
Outputs
__________________________________________________
| id (serial) | pub_id (text) | tweet (text) |
|---------------|-----------------|----------------|
| 1 | abc | hello world |
| 2 | def | blurb |
--------------------------------------------------
*/