How do you use variables in a simple PostgreSQL script? - postgresql

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 |
--------------------------------------------------
*/

Related

How avoid extra quotes added within a loop in postgresql

When performing a loop on a table in which a value has ", I get those quotes doubled such as:
initial input: 'test AND "test"'
output in the loop: 'test AND ""test""'
How to reproduce:
CREATE TABLE roles(
id int,
criteria VARCHAR (255)
);
INSERT INTO roles (id, criteria) VALUES (1, 'test AND "test"');
CREATE OR REPLACE FUNCTION test_quote()
RETURNS VARCHAR
LANGUAGE plpgsql
AS $$
DECLARE
sample varchar;
rec record;
BEGIN
FOR rec IN
SELECT * FROM roles
LOOP
sample:= rec;
END LOOP;
RETURN sample;
END
$$;
SELECT * FROM test_quote();
Expected result: (1,"test AND "test"")
Actual result: (1,"test AND ""test""")
Does anyone have an idea how to get the expected behaviour here?
Found it, indeed, the issue is from the conversion from record to varchar.
I can directly pass the column from the rec like:
sample:= rec.criteria;
(I didn't know that we could do that)
So, that gives:
CREATE TABLE roles(
id int,
criteria VARCHAR (255)
);
INSERT INTO roles (id, criteria) VALUES (1, 'test AND "test"');
CREATE OR REPLACE FUNCTION test_quote()
RETURNS VARCHAR
LANGUAGE plpgsql
AS $$
DECLARE
sample varchar;
rec record;
BEGIN
FOR rec IN
SELECT * FROM roles
LOOP
sample := rec.criteria;
END LOOP;
RETURN sample;
END
$$;
SELECT * FROM test_quote();
Thx guys!

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.

Triggers in Postgres: Access NEW fields by name at runtime

In Postgres, someone knows how to substitute the value of the variable in a NEW.variable in a trigger?
For instance, I have a variable with value order_code. I want to execute NEW.variable so that it's getting in fact NEW.order_code.
In detailed:
I have a function to obtain the primary key column of a table:
CREATE FUNCTION getPrimaryKey(_table_name VARCHAR(50))
RETURNS SETOF VARCHAR(50) AS $$
DECLARE
primary_key VARCHAR(50);
BEGIN
FOR primary_key IN SELECT a.attname
FROM pg_index i
JOIN pg_attribute a ON a.attrelid = i.indrelid
AND a.attnum = ANY(i.indkey)
WHERE i.indrelid = _table_name::regclass
AND i.indisprimary LOOP
RETURN NEXT primary_key;
END LOOP;
END;
$$ LANGUAGE plpgsql;
Then I have a trigger to collect some info when an INSERT is done in a table. The procedure in the trigger is called from several triggers from different tables. That's why it's so generic and I have this need.
What I want is to obtain the primary key of the object inserted.
CREATE FUNCTION logAudit()
RETURNS trigger AS $$
DECLARE primary_key VARCHAR(50);
BEGIN
primary_key := getprimarykey(TG_TABLE_NAME::VARCHAR(50));
INSERT INTO test VALUES (TG_TABLE_NAME);
INSERT INTO test VALUES (NEW.primary_key);
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER audit_in_client
AFTER INSERT ON tb_client
FOR EACH STATEMENT EXECUTE PROCEDURE logAudit();
The NEW.primary_key is what is causing me issues. I expect primary_key to be the column name of the source table where the insert happened. What I want in NEW.primary_key is to actually use the value in the variable.
Here is the example of anonymous pl/pgsql block which doing something what you want:
do $$
declare
v pg_database = (pg_database) from pg_database where datname = 'template1';
fname text = 'datname';
n text;
begin
n := to_jsonb(v)->>fname;
raise info '%', n;
end $$;
Output:
INFO: template1
It is working example. In your trigger function it could be something like
declare
pk_name text;
pk_value text;
begin
pk_name := getprimarykey(TG_TABLE_NAME::VARCHAR(50));
pk_value := to_jsonb(NEW) ->> pk_name;
-- Do what you want with pk_value here
return null;
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 $$;

Check if column exists when there are multiple tables with same name in different schemas (PSQL 8.2)

In order to check if a column exists I can easily use something similar to this:
SELECT attname FROM pg_attribute
WHERE attrelid = (SELECT oid FROM pg_class WHERE relname = 'YOURTABLENAME')
AND attname = 'YOURCOLUMNNAME';
However, I run into problems with
SELECT oid FROM pg_class WHERE relname = 'YOURTABLENAME'
When there are several tables with the same name spread out over different schemas as it returns the OID's of all the tables with that name. How do I check if a table within a specific schema contains the column I am after? I'm using Postgres 8.2.
8.2 supports information_schema views. Something along these lines should work. You'll have to supply your own database, schema, table, and column names, of course.
select *
from information_schema.columns
where table_catalog = 'sandbox'
and table_schema = 'public'
and table_name = 'calendar'
and column_name = 'iso_year';
Generic solution, for checking if table or column exists, is using functions (see below).
Here I use also a function to parse the table name.
PS: the easy way to check table is using in your function the ::regclass, see the optional table_exists_v2() function.
CREATE FUNCTION tname_split(
-- Verify the table name, sort, use defaults if necessary, and returns
-- array with indexes: 1=complete name, 2=schema name, 3=table name.
p_tabname varchar, -- table name or full name (schema.table)
p_schema varchar DEFAULT 'public' -- default schema name
) RETURNS varchar[] AS $func$
DECLARE
p integer;
BEGIN
IF (p_schema IS NULL) THEN p_schema:='public'; END IF;
p := strpos(p_tabname,'.'); -- check schema
IF p=0 AND p_schema='' THEN
RAISE EXCEPTION 'ERROR, schema name default can not be empty';
ELSEIF p=0 THEN
RETURN ARRAY[p_schema||'.'||p_tabname,p_schema,p_tabname];
ELSEIF p=1 THEN
RAISE EXCEPTION 'ERROR, table name initiating by dot';
ELSE
RETURN ARRAY[p_tabname] || string_to_array(p_tabname, '.')::varchar[];
END IF;
END;
$func$ LANGUAGE plpgsql;
CREATE FUNCTION column_exists(
--
-- Check if a column exists in a table. Returns true if yes.
--
p_colname varchar,
p_tname varchar, -- table name or full name (schema.table)
p_schema varchar DEFAULT NULL -- default schema name
) RETURNS BOOLEAN AS $func$
DECLARE
t varchar[];
BEGIN
t = lib.tadm_tname_split(p_tname,p_schema);
PERFORM 1 FROM information_schema.COLUMNS
WHERE column_name=$1 AND table_schema=t[2] AND table_name=t[3];
RETURN FOUND;
END;
$func$ LANGUAGE plpgsql;
CREATE FUNCTION table_exists(
--
-- Check if a table exists, returns true if yes, false if not.
--
p_tname varchar, -- table name or full name (schema.table)
p_schema varchar DEFAULT 'public', -- default schema name
) RETURNS BOOLEAN AS $func$
DECLARE
t varchar[];
BEGIN
t = lib.tadm_tname_split(p_tname,p_schema);
RETURN EXISTS (SELECT tablename FROM pg_tables WHERE schemaname=t[2] AND tablename=t[3]);
END;
$func$ LANGUAGE plpgsql;
CREATE FUNCTION table_exists_v2(p_tname varchar) RETURNS boolean AS $f$
DECLARE
tmp regclass;
BEGIN
tmp := p_tname::regclass; -- do nothing, only parsing regclass
RETURN true;
EXCEPTION WHEN SQLSTATE '3F000' THEN
RETURN false;
END;
$f$ LANGUAGE plpgsql;