Improving PL/pgSQL function - postgresql

I just finished writing my first PLSQL function. Here what it does.
The SQL function attempt to reset the duplicate timestamp to NULL.
From table call_records find all timestamp that are duplicated.(using group by)
loop through each timestamp.Find all record with same timestamp (times-1, so that only 1 record for a given times is present)
From all the records found in step 2 update the timestamp to NULL
Here how the SQL function looks like.
CREATE OR REPLACE FUNCTION nullify() RETURNS INTEGER AS $$
DECLARE
T call_records.timestamp%TYPE;
-- Not sure why row_type does not work
-- R call_records%ROWTYPE;
S integer;
CRNS bigint[];
TMPS bigint[];
sql_stmt varchar = '';
BEGIN
FOR T,S IN (select timestamp,count(timestamp) as times from call_records where timestamp IS NOT NULL group by timestamp having count(timestamp) > 1)
LOOP
sql_stmt := format('SELECT ARRAY(select plain_crn from call_records where timestamp=%s limit %s)',T,S-1);
EXECUTE sql_stmt INTO TMPS;
CRNS := array_cat(CRNS,TMPS);
END LOOP;
sql_stmt = format('update call_records set timestamp=null where plain_crn in (%s)',array_to_string(CRNS,','));
RAISE NOTICE '%',sql_stmt;
EXECUTE sql_stmt ;
RETURN 1;
END
$$ LANGUAGE plpgsql;
Help me understand more PL/pgSQL language my suggesting me how it can be done better.
#a_horse_with_no_name: Here how the DB structure looks like
\d+ call_records;
id integer primary key
plain_crn bigint
timestamp bigint
efd integer default 0
id | efd | plain_crn | timestamp
----------+------------+------------+-----------
1 | 2016062936 | 8777444059 | 14688250050095
2 | 2016062940 | 8777444080 | 14688250050095
3 | 2016063012 | 8880000000 | 14688250050020
4 | 2016043011 | 8000000000 | 14688240012012
5 | 2016013011 | 8000000001 | 14688250050020
6 | 2016022011 | 8440000001 |
Now,
select timestamp,count(timestamp) as times from call_records where timestamp IS NOT NULL group by timestamp having count(timestamp) > 1
timestamp | count
-----------------+-----------
14688250050095 | 2
14688250050020 | 2
All that I want is to update the duplicate timestamp to null so that only one of them record has the given timestamp.
In short the above query should return result like this
select timestamp,count(timestamp) as times from call_records where timestamp IS NOT NULL group by timestamp;
timestamp | count
-----------------+-----------
14688250050095 | 1
14688250050020 | 1

You can use array variables directly (filter with predicate =ANY() - using dynamic SQL is wrong for this purpose:
postgres=# DO $$
DECLARE x int[] = '{1,2,3}';
result int[];
BEGIN
SELECT array_agg(v)
FROM generate_series(1,10) g(v)
WHERE v = ANY(x)
INTO result;
RAISE NOTICE 'result is: %', result;
END;
$$;
NOTICE: result is: {1,2,3}
DO
Next - this is typical void function - it doesn't return any interesting. Usually these functions returns nothing when all is ok or raises exception. The returning 1 RETURN 1 is useless.
CREATE OR REPLACE FUNCTION foo(par int)
RETURNS void AS $$
BEGIN
IF EXISTS(SELECT * FROM footab WHERE id = par)
THEN
...
ELSE
RAISE EXCEPTION 'Missing data for parameter: %', par;
END IF;
END;
$$ LANGUAGE plpgsql;

Related

How to return result of dynamic SELECT inside a function in PostgreSQL?

A very similar question here but not quite the same as this one.
I have a function that uses IF statements to determine what type of SELECT query to return.
How can I declare what a CREATE FUNCTION statment should return when I will never know the exact columns a SELECT query within it might return? That is, I can't setup a RETURNS TABLE declaration with a list of columns because I don't know which columns might come back. All I know is that I definitely will want a table of results to be returned.
Here is my function (uncompleted, pseudo):
CREATE OR REPLACE FUNCTION functiona(_url character varying DEFAULT NULL)
RETURNS -- what type? if TABLE how do I know what columns to specify
LANGUAGE plpgsql
AS
$$
DECLARE
_urltypeid int;
BEGIN
IF _url IS NOT NULL
THEN
_urltypeid := reference.urltype(_url);
IF _urltypeid = 1
THEN
RETURN QUERY
SELECT location, auxiliary, response FROM tablea -- unique columns from one table
END IF;
IF _urltypeid = 2
THEN
RETURN QUERY
SELECT ip, location, host, authority FROM tableb -- unique columns from another table
END IF;
END IF;
END;
$$;
I come from a MS SQL Server background where I don't have to specify in the CREATE FUNCTIONstatement what I'm returning, hence this is very confusing for me.
Not an answer, but an explanation of why answer from #JonathanJacobson will not work using a simple example:
\d animals
Table "public.animals"
Column | Type | Collation | Nullable | Default
--------+------------------------+-----------+----------+---------
id | integer | | not null |
cond | character varying(200) | | not null |
animal | character varying(200) | | not null |
CREATE OR REPLACE FUNCTION public.animal(a_type character varying)
RETURNS record
LANGUAGE plpgsql
AS $function$
BEGIN
SELECT row(id, cond, animal) FROM animals where animal = a_type;
END;
$function$
select * from animal('cat');
ERROR: a column definition list is required for functions returning "record"
LINE 1: select * from animal('cat');
CREATE OR REPLACE FUNCTION public.animal(a_type character varying)
RETURNS SETOF record
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT id, cond, animal FROM animals where animal = a_type;
END;
$function$
;
select * from animal('cat') as t(i integer, c varchar, a varchar);
i | c | a
---+------+-----
1 | fat | cat
2 | slim | cat
6 | big | cat
In order to use the output of a function returning a record or setof record you need to declare the output fields and types when you run the function.
You could use the record type. Not tested.
CREATE OR REPLACE FUNCTION functiona(_url character varying DEFAULT NULL)
RETURNS record
LANGUAGE plpgsql
AS
$$
DECLARE
_broadcasttypeid int;
BEGIN
IF _url IS NOT NULL
THEN
_urltypeid := reference.urltype(_url);
IF _urltypeid = 1
THEN
RETURN
(SELECT row(location, auxiliary, response) FROM tablea);
END IF;
IF _urltypeid = 2
THEN
RETURN
(SELECT row(ip, location, host, authority) FROM tableb);
END IF;
END IF;
END;
$$;
Other composite types, such as jsonb and hstore are also a solution.

function to provide id of interval the input is located between

I want to create a function that will receive a date as input and be able to provide back the id of the corresponding interval that it's located in.
For example with this table here:
id | start | end
-- +------------+------------
1 | 2000-11-30 | 2001-02-19
2 | 2001-02-21 | 2001-06-04
3 | 2001-06-05 | 2001-07-13
4 | 2001-07-15 | 2001-11-29
If i input the date '2001-04-17', i want it to return back the id value of 2.
i'm currently trying this currently but can't get it to work:
create or replace function getId(_date date) returns integer
as $$
declare
myId integer;
begin
set myId = (select id from myTable
where ((_date >= start) and (_date <= end)));
return myId;
end;
$$ language plpgsql
;
You can return id directly without setting to myId. Andend is the PostgreSQL keyword, so you should use it inside double quotes ""
create or replace function getId(_date date) returns integer
as $$
begin
return (select id from myTable
where ((_date >= start) and (_date <= "end")));
end;
$$ language plpgsql
;
Call the function:
select getId('2001-04-17');
Output: 2

Multiple result sets from a stored procedure with PostgreSQL

Getting unexpected result from function. I just need two result sets from the code that I have written in the function but instead getting some unnamed portal issue.
I have tried same using cursor.Which is as follows.
CREATE OR REPLACE FUNCTION User(param_state CHAR(10)) RETURNS SETOF refcursor AS $$
DECLARE
ref1 refcursor; -- Declare cursor variables
ref2 refcursor;
BEGIN
OPEN ref1 FOR select * from Table1
WHERE code = param_state;
RETURN NEXT ref1;
OPEN ref2 FOR select * from Table2
WHERE code= param_state;
RETURN NEXT ref2;
END;
$$ LANGUAGE plpgsql;
Expected output should be to 2 result set of 2 column each
-------------------
|party_code | limit|
|------------------|
|T001 | 120 |
-------------------
-------------------
|party_code | Sal |
|------------------|
|T001 | 1000 |
-------------------
But the output is
---------------------
|<unnamed portal 34>|
---------------------
|<unnamed portal 35>|
Have you tried to name your cursors ..
...
DECLARE
ref1 refcursor := 'mycursor1' ;
ref2 refcursor := 'mycursor2' ;
...
.. and fetch the results using their names ..
SELECT * FROM "User"('T001');
BEGIN;
FETCH ALL FROM mycursor2;
FETCH ALL FROM mycursor1;
END;
FETCH ALL FROM mycursor2;
code | Sal
------+------
T001 | 1000
(1 row)
postgres=# FETCH ALL FROM mycursor1;
code | limit
------+-------
T001 | 120
(1 row)

Create dynamic tables based on a loop in PostgreSQL 9.2

I have a function where I want to create a table for a every year based on the year from bill date which I will be looping.
CREATE OR REPLACE FUNCTION ccdb.ccdb_archival()
RETURNS void AS
$BODY$
DECLARE dpsql text;
DECLARE i smallint;
BEGIN
FOR i IN SELECT DISTINCT EXTRACT(year FROM bill_date) FROM ccdb.bills ORDER BY 1 LOOP
DO $$
BEGIN
CREATE TABLE IF NOT EXISTS ccdb_archival.bills||i (LIKE ccdb.bills INCLUDING ALL);
BEGIN
ALTER TABLE ccdb_archival.bills ADD COLUMN archival_date timestamp;
EXCEPTION
WHEN duplicate_column THEN RAISE NOTICE 'column archival_date already exists in <table_name>.';
END;
END;
$$;
INSERT INTO ccdb_archival.bills
SELECT *, now() AS archival_date
FROM ccdb.bills
WHERE bill_date::date >= current_date - interval '3 years' AND bill_date::date < current_date - interval '8 years';
END LOOP;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
I want to concatenate the year with the actual table name for each year.
I am unable to do the same with the above code. I get an error:
ERROR: syntax error at or near "||"
LINE 3: CREATE TABLE IF NOT EXISTS ccdb_archival.bills||i (LI...
Please suggest how do I achieve my requirement.
you cannot compose strings with metadata. You should utilize execute: http://www.postgresql.org/docs/9.1/static/ecpg-sql-execute-immediate.html
To create N tables with a prefix use this script.
This code uses a for loop and variable to creates 10 table starting with prefix 'sbtest' namely sbtest1, sbtest2 ... sbtest10
create_table.sql
do $$
DECLARE myvar integer;
begin
for myvar in 1..10 loop
EXECUTE format('CREATE TABLE sbtest%s (
id SERIAL NOT NULL,
k INTEGER NOT NULL,
c CHAR(120) NOT NULL,
pad CHAR(60) NOT NULL,
PRIMARY KEY (id))', myvar);
end loop;
end; $$
Run it using psql -U user_name -d database_name -f create_table.sql
Example Table sbtest1 is as
id | k | c | pad
----+---+---+-----
(0 rows)
Table "public.sbtest1"
Column | Type | Collation | Nullable | Default | Storage | Stats
target | Description
--------+----------------+-----------+----------+-------------------------------------+----------+------
--------+-------------
id | integer | | not null | nextval('sbtest1_id_seq'::regclass) | plain |
|
k | integer | | not null | | plain |
|
c | character(120) | | not null | | extended |
|
pad | character(60) | | not null | | extended |
|
Indexes:
"sbtest1_pkey" PRIMARY KEY, btree (id)
Access method: heap

Update tables logic

I have two tables with triggers on them.
FIRST
CREATE OR REPLACE FUNCTION update_table()
RETURNS trigger AS
$BODY$
BEGIN
IF TG_OP = 'UPDATE' THEN
UPDATE filedata SET id=NEW.id,myData=NEW.myData,the_geom=ST_TRANSFORM(NEW.the_geom,70066) WHERE num=NEW.num;
RETURN NEW;
ELSEIF TG_OP = 'INSERT' THEN
INSERT INTO filedata(num,id,myData,the_geom) VALUES (NEW.num,NEW.id,NEW.myData,ST_TRANSFORM(NEW.the_geom,70066));
INSERT INTO filestatus(id,name,status) VALUES (NEW.num,NEW.myData,'Не подтвержден');
RETURN NEW;
END IF;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
SECOND
CREATE OR REPLACE FUNCTION update_table_temp()
RETURNS trigger AS
$BODY$
BEGIN
IF TG_OP = 'INSERT' THEN
INSERT INTO filedata_temp(num,id,myData,the_geom) VALUES (NEW.num,NEW.id,NEW.myData,ST_TRANSFORM(NEW.the_geom,900913));
RETURN NEW;
ELSEIF TG_OP = 'DELETE' THEN
DELETE FROM filedata_temp WHERE num=OLD.num;
RETURN OLD;
END IF;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
And I have a problem. If I insert data in the first table its trigger inserts data in the second table too. But that insert causes the second table's trigger to do an insert on the first table, and so on.
Can you help me with this? How to can I get the tables to update each other without looping?
UPDATE
i have another problem
How to change data when i INSERT it in table? For example i insert GEOMETRY in the_geom column. And if geometry's SRID=70066 i want to put in the_geom column result of working of this function ST_TRANSFORM(the_geom,900913).
UPDATE 2
trigger
CREATE TRIGGER update_geom
AFTER INSERT
ON filedata_temp
FOR EACH ROW
EXECUTE PROCEDURE update_geom();
function
CREATE OR REPLACE FUNCTION update_geom()
RETURNS trigger AS
$$
BEGIN
IF ST_SRID(NEW.the_geom)=70066 THEN
UPDATE filedata_temp SET id='88',the_geom=ST_TRANSFORM(NEW.the_geom,900913);
END IF;
RETURN NEW;
END;
$$
LANGUAGE plpgsql;
If i use this function trigger no work but if this:
CREATE OR REPLACE FUNCTION update_geom()
RETURNS trigger AS
$$
BEGIN
UPDATE filedata_temp SET id='88',the_geom=ST_TRANSFORM(NEW.the_geom,900913);
RETURN NEW;
END;
$$
LANGUAGE plpgsql;
i get id=88 but ST_TRANSFORM not work.
UPDATE 3
ST_TRANSFORM() nice function but its do something strange in my case.
For example i have a table filedata_temp(SRID=4326). I Insert geometry with srid=70066 i try this trigger
CREATE OR REPLACE FUNCTION update_geom()
RETURNS trigger AS
$$
BEGIN
UPDATE filedata_temp the_geom=ST_TRANSFORM(NEW.the_geom,4326);
RETURN NEW;
END;
$$
LANGUAGE plpgsql;
And get this geometry.
"0103000020E6100000010000001800000097832C7ABD823741DA312CBF59F6174145ED23E0088337413CB8228A65F7174145ED23E0088337413CB8228A65F7174145ED23E0088337413CB8228A65F7174115B8A7F8208337416DE8C689ADF7174115B8A7F8208337416DE8C689ADF71741D1D3BAF56383374114BFD1303AF917418B016D395F8537413C2856DFF7F717413AF95F044C853741A997BC22A3F71741F88E75BD2D85374178C92D9BE6F61741F92A3B192685374165C8D76E31F61741C84AA37B26853741F2674F6A96F5174144F25B9B16853741E849D10C1BF5174105142CD2E384374112E19E8688F31741B72C78F697843741A808260138F31741FF0C0C6A0884374151A8BBFF76F21741832CF2EEF48337418BE15C1290F21741FFFB3AC6A3833741D85A253DF4F2174113E8B8956C83374109067F2139F31741E383648E3383374100C25C64D8F3174179BAEBD7178337412DA0D6482BF41741CF38E4F7038337410AB04BD7E5F41741C936158CE182374145C1EC5D99F5174197832C7ABD823741DA312CBF59F61741"
ST_transform() make this string from SRID=4326 and geometry which transform in EPSG:70066.
There is this string in 70066
"0103000020B2110100010000001800000097832C7ABD823741DA312CBF59F6174145ED23E0088337413CB8228A65F7174145ED23E0088337413CB8228A65F7174145ED23E0088337413CB8228A65F7174115B8A7F8208337416DE8C689ADF7174115B8A7F8208337416DE8C689ADF71741D1D3BAF56383374114BFD1303AF917418B016D395F8537413C2856DFF7F717413AF95F044C853741A997BC22A3F71741F88E75BD2D85374178C92D9BE6F61741F92A3B192685374165C8D76E31F61741C84AA37B26853741F2674F6A96F5174144F25B9B16853741E849D10C1BF5174105142CD2E384374112E19E8688F31741B72C78F697843741A808260138F31741FF0C0C6A0884374151A8BBFF76F21741832CF2EEF48337418BE15C1290F21741FFFB3AC6A3833741D85A253DF4F2174113E8B8956C83374109067F2139F31741E383648E3383374100C25C64D8F3174179BAEBD7178337412DA0D6482BF41741CF38E4F7038337410AB04BD7E5F41741C936158CE182374145C1EC5D99F5174197832C7ABD823741DA312CBF59F61741"
And in 4326
"0103000020E61000000100000018000000AE4F5BA2FC5B4E407E80E7E6F46C4C40F7F1BF79255C4E4019C32D62086D4C40F7F1BF79255C4E4019C32D62086D4C40F7F1BF79255C4E4019C32D62086D4C40A7CE9382325C4E40D8EA369C0D6D4C40A7CE9382325C4E40D8EA369C0D6D4C401BD2B101575C4E4064A420982A6D4C4090DF29FE665D4E4064EE5369116D4C408195B3905C5D4E403664043C0B6D4C4025A00D0E4C5D4E40F7FD7274FD6C4C404201C7B5475D4E409ADF7B26F06C4C403801C7B5475D4E40E43D0EBFE46C4C406EC339053F5D4E404085D2B7DB6C4C40BDFDA836235D4E4001EBC841BE6C4C40685B445FFA5C4E4015C4038EB86C4C40ADB5C108AD5C4E40727935C6AA6C4C408A6B4B9BA25C4E40331ECEACAC6C4C40A7368928775C4E40F7C22E47B46C4C409F640F9D595C4E4077694F81B96C4C40660C21333B5C4E4012EA7C62C56C4C406623646D2C5C4E40CE83E38FCB6C4C4042D9EDFF215C4E40C6A89957D96C4C4095D75EC00F5C4E4013FFA0A5E66C4C40AE4F5BA2FC5B4E407E80E7E6F46C4C40"
You have mutually recursive triggers and you want to prevent the recursion. Your instead want a trigger to fire only on a direct action from a user, not an action via a trigger.
Unfortunately, PostgreSQL doesn't directly support what you want, you'll need to tweak your design to avoid the mutual recursion.
Updated question: In a trigger, alter the contents of NEW, eg
IF tg_op = 'INSERT' OR tg_op = 'UPDATE' THEN
NEW.the_geom := ST_TRANSFORM(NEW.the_geom,900913)
END IF;
See the really rather good manual for triggers.
-- The scenario is:
-- for UPDATEs we use an "alternating bit protocol"
-- (could also be done by bumping and synchronisng a serial number)
-- For INSERTs: we only test for NOT EXISTS.
-- DELETEs are not yet implemented.
-- *******************************************************************
DROP SCHEMA tmp CASCADE;
CREATE SCHEMA tmp ;
SET search_path=tmp;
--
-- Tables for test: we convert int <<-->> text
--
CREATE TABLE one
( id INTEGER NOT NULL PRIMARY KEY
, flipflag boolean NOT NULL default false
, ztext varchar
);
CREATE TABLE two
( id INTEGER NOT NULL PRIMARY KEY
, flipflag boolean NOT NULL default false
, zval INTEGER
);
------------------------
CREATE function func_one()
RETURNS TRIGGER AS $body$
BEGIN
IF tg_op = 'INSERT' THEN
INSERT INTO two (id,zval)
SELECT NEW.id, NEW.ztext::integer
WHERE NOT EXISTS (
SELECT * FROM two WHERE two.id = NEW.id)
;
ELSIF tg_op = 'UPDATE' THEN
UPDATE two
SET zval = NEW.ztext::integer
, flipflag = NOT flipflag
WHERE two.id = NEW.id
;
END IF;
RETURN NEW;
END;
$body$
language plpgsql;
CREATE TRIGGER trig_one_i
AFTER INSERT ON one
FOR EACH ROW
EXECUTE PROCEDURE func_one()
;
CREATE TRIGGER trig_one_u
AFTER UPDATE ON one
FOR EACH ROW
WHEN (NEW.flipflag = OLD.flipflag)
EXECUTE PROCEDURE func_one()
;
------------------------
CREATE function func_two()
RETURNS TRIGGER AS $body$
BEGIN
IF tg_op = 'INSERT' THEN
INSERT INTO one (id,ztext)
SELECT NEW.id, NEW.zval::varchar
WHERE NOT EXISTS (
SELECT * FROM one WHERE one.id = NEW.id)
;
ELSIF tg_op = 'UPDATE' THEN
UPDATE one
SET ztext = NEW.zval::varchar
, flipflag = NOT flipflag
WHERE one.id = NEW.id
;
END IF;
RETURN NEW;
END;
$body$
language plpgsql;
CREATE TRIGGER trig_two_i
AFTER INSERT ON two
FOR EACH ROW
EXECUTE PROCEDURE func_two()
;
CREATE TRIGGER trig_two_u
AFTER UPDATE ON two
FOR EACH ROW
WHEN (NEW.flipflag = OLD.flipflag)
EXECUTE PROCEDURE func_two()
; --
-- enter some data
--
INSERT INTO one (id,ztext)
select gs, gs::text
FROM generate_series(1,10) gs
;
-- Change some data
UPDATE one SET ztext=100 where id = 1;
UPDATE two SET zval=10*zval where id IN (2,4,6,8,10);
INSERT INTO two (id, zval) VALUES(11,14);
SELECT * FROM one ORDER BY id;
SELECT * FROM two ORDER BY id;
RESULT:
NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "one_pkey" for table "one"
CREATE TABLE
NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "two_pkey" for table "two"
CREATE TABLE
CREATE FUNCTION
CREATE TRIGGER
CREATE TRIGGER
CREATE FUNCTION
CREATE TRIGGER
CREATE TRIGGER
INSERT 0 10
UPDATE 1
UPDATE 5
INSERT 0 1
id | flipflag | ztext
----+----------+-------
1 | f | 100
2 | t | 20
3 | f | 3
4 | t | 40
5 | f | 5
6 | t | 60
7 | f | 7
8 | t | 80
9 | f | 9
10 | t | 100
11 | f | 14
(11 rows)
id | flipflag | zval
----+----------+------
1 | t | 100
2 | f | 20
3 | f | 3
4 | f | 40
5 | f | 5
6 | f | 60
7 | f | 7
8 | f | 80
9 | f | 9
10 | f | 100
11 | f | 14
(11 rows)