Loop through columns of RECORD - postgresql

I need to loop through type RECORD items by key/index, like I can do this using array structures in other programming languages.
For example:
DECLARE
data1 record;
data2 text;
...
BEGIN
...
FOR data1 IN
SELECT
*
FROM
sometable
LOOP
FOR data2 IN
SELECT
unnest( data1 ) -- THIS IS DOESN'T WORK!
LOOP
RETURN NEXT data1[data2]; -- SMTH LIKE THIS
END LOOP;
END LOOP;

As #Pavel explained, it is not simply possible to traverse a record, like you could traverse an array. But there are several ways around it - depending on your exact requirements. Ultimately, since you want to return all values in the same column, you need to cast them to the same type - text is the obvious common ground, because there is a text representation for every type.
Quick and dirty
Say, you have a table with an integer, a text and a date column.
CREATE TEMP TABLE tbl(a int, b text, c date);
INSERT INTO tbl VALUES
(1, '1text', '2012-10-01')
,(2, '2text', '2012-10-02')
,(3, ',3,ex,', '2012-10-03') -- text with commas
,(4, '",4,"ex,"', '2012-10-04') -- text with commas and double quotes
Then the solution can be a simple as:
SELECT unnest(string_to_array(trim(t::text, '()'), ','))
FROM tbl t;
Works for the first two rows, but fails for the special cases of row 3 and 4.
You can easily solve the problem with commas in the text representation:
SELECT unnest(('{' || trim(t::text, '()') || '}')::text[])
FROM tbl t
WHERE a < 4;
This would work fine - except for line 4 which has double quotes in the text representation. Those are escaped by doubling them up. But the array constructor would need them escaped by \. Not sure why this incompatibility is there ...
SELECT ('{' || trim(t::text, '()') || '}') FROM tbl t WHERE a = 4
Yields:
{4,""",4,""ex,""",2012-10-04}
But you would need:
SELECT '{4,"\",4,\"ex,\"",2012-10-04}'::text[]; -- works
Proper solution
If you knew the column names beforehand, a clean solution would be simple:
SELECT unnest(ARRAY[a::text,b::text,c::text])
FROM tbl
Since you operate on records of well know type you can just query the system catalog:
SELECT string_agg(a.attname || '::text', ',' ORDER BY a.attnum)
FROM pg_catalog.pg_attribute a
WHERE a.attrelid = 'tbl'::regclass
AND a.attnum > 0
AND a.attisdropped = FALSE
Put this in a function with dynamic SQL:
CREATE OR REPLACE FUNCTION unnest_table(_tbl text)
RETURNS SETOF text LANGUAGE plpgsql AS
$func$
BEGIN
RETURN QUERY EXECUTE '
SELECT unnest(ARRAY[' || (
SELECT string_agg(a.attname || '::text', ',' ORDER BY a.attnum)
FROM pg_catalog.pg_attribute a
WHERE a.attrelid = _tbl::regclass
AND a.attnum > 0
AND a.attisdropped = false
) || '])
FROM ' || _tbl::regclass;
END
$func$;
Call:
SELECT unnest_table('tbl') AS val
Returns:
val
-----
1
1text
2012-10-01
2
2text
2012-10-02
3
,3,ex,
2012-10-03
4
",4,"ex,"
2012-10-04
This works without installing additional modules. Another option is to install the hstore extension and use it like #Craig demonstrates.

PL/pgSQL isn't really designed for what you want to do. It doesn't consider a record to be iterable, it's a tuple of possibly different and incompatible data types.
PL/pgSQL has EXECUTE for dynamic SQL, but EXECUTE queries cannot refer to PL/pgSQL variables like NEW or other records directly.
What you can do is convert the record to a hstore key/value structure, then iterate over the hstore. Use each(hstore(the_record)), which produces a rowset of key,value tuples. All values are cast to their text representations.
This toy function demonstrates iteration over a record by creating an anonymous ROW(..) - which will have column names f1, f2, f3 - then converting that to hstore, iterating over its column/value pairs, and returning each pair.
CREATE EXTENSION hstore;
CREATE OR REPLACE FUNCTION hs_demo()
RETURNS TABLE ("key" text, "value" text)
LANGUAGE plpgsql AS
$$
DECLARE
data1 record;
hs_row record;
BEGIN
data1 = ROW(1, 2, 'test');
FOR hs_row IN SELECT kv."key", kv."value" FROM each(hstore(data1)) kv
LOOP
"key" = hs_row."key";
"value" = hs_row."value";
RETURN NEXT;
END LOOP;
END;
$$;
In reality you would never write it this way, since the whole loop can be replaced with a simple RETURN QUERY statement and it does the same thing each(hstore) does anyway - so this is only to show how each(hstore(record)) works, and the above function should never actually be used.

This feature is not supported in plpgsql - Record IS NOT hash array like other scripting languages - it is similar to C or ADA, where this functionality is impossible. You can use other PL language like PLPerl or PLPython or some tricks - you can iterate with HSTORE datatype (extension) or via dynamic SQL
see How to set value of composite variable field using dynamic SQL
But request for this functionality usually means, so you do some wrong. When you use PL/pgSQL you have think different than you use Javascript or Python

FOR data2 IN
SELECT d
from unnest( data1 ) s(d)
LOOP
RETURN NEXT data2;
END LOOP;

If you order your results prior to looping, will you accomplish what you want.
for rc in select * from t1 order by t1.key asc loop
return next rc;
end loop;
will do exactly what you need. It is also the fastest way to perform that kind of task.

I wasn't able to find a proper way to loop over record, so what I did is converted record to json first and looped over json
declare
_src_schema varchar := 'db_utility';
_targetjson json;
_key text;
_value text;
BEGIN
select row_to_json(c.*) from information_schema.columns c where c.table_name = prm_table and c.column_name = prm_column
and c.table_schema = _src_schema into _targetjson;
raise notice '_targetjson %', _targetjson;
FOR _key, _value IN
SELECT * FROM jsonb_each_text(_targetjson)
LOOP
-- do some math operation on its corresponding value
RAISE NOTICE '%: %', _key, _value;
END LOOP;
return true;
end;

Related

Fill a variable with "array_to_string" in a plpgsql trigger function

I'm working with PostgreSQL 9.5.
I'm creating a trigger in PL/pgSQL, that adds a record to a table (synthese_poly) when an INSERT is performed on a second table (operation_poly), with other tables data.
The trigger works well, except for some variables, that are not filled (especially the ones I try to fill with an array_to_string() function).
This is the code:
-- Function: bdtravaux.totablesynth_fn()
-- DROP FUNCTION bdtravaux.totablesynth_fn();
CREATE OR REPLACE FUNCTION bdtravaux.totablesynth_fn()
RETURNS trigger AS
$BODY$
DECLARE
varoperateur varchar;
varchantvol boolean;
BEGIN
IF (TG_OP = 'INSERT') THEN
varsortie_id := NEW.sortie;
varopeid := NEW.operation_id;
--The following « SELECT » queries take data in third-party tables and fill variables, which will be used in the final insertion query.
SELECT array_to_string(array_agg(DISTINCT oper.operateurs),'; ')
INTO varoperateur
FROM bdtravaux.join_operateurs oper INNER JOIN bdtravaux.operation_poly o ON (oper.id_joinop=o.id_oper)
WHERE o.operation_id = varopeid;
SELECT CASE WHEN o.ope_chvol = 0 THEN 'f' ELSE 't' END as opechvol INTO varchantvol
FROM bdtravaux.operation_poly o WHERE o.operation_id = varopeid;
-- «INSERT» query
INSERT INTO bdtravaux.synthese_poly (soperateur, schantvol) SELECT varoperateur, varchantvol;
RAISE NOTICE 'varoperateur value : (%)', varoperateur;
RAISE NOTICE 'varchantvol value : (%)', varchantvol;
END IF;
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
ALTER FUNCTION bdtravaux.totablesynth_fn()
OWNER TO postgres;
And this is the trigger :
-- Trigger: totablesynth on bdtravaux.operation_poly
-- DROP TRIGGER totablesynth ON bdtravaux.operation_poly;
CREATE TRIGGER totablesynth
AFTER INSERT
ON bdtravaux.operation_poly
FOR EACH ROW
WHEN ((new.chantfini = true))
EXECUTE PROCEDURE bdtravaux.totablesynth_fn();
The varchantvol variable is correctly filled, but varoperateur stays desperately empty (NULL value) (and so on for the corresponding field in the synthese_poly table).
Note:
The SELECT array_to_string(…) ... query itself (launched with pgAdmin, without INTO varoperateur and replacing varopeid with a value) works well, and returns a string.
I tried to change array_to_string() function and variables' data types (using ::varchar or ::text …), nothing works.
Do you see what can happen?
using array_agg
You can replace array_to_string(array_agg(DISTINCT oper.operateurs),'; ') with
string_agg(DISTINCT oper.operateurs,'; ')
And you can use order by to sort the text in the agregate
string_agg(DISTINCT oper.operateurs,'; ' ORDER BY oper.operateurs)
My educated guess: you have a trigger with BEFORE INSERT ON bdtravaux.operation_poly. And operation_id is its serial PK column.
In this case, the query with WHERE o.operation_id = varopeid
(where varopeid has been filled with NEW.operation_id) can never find any rows because the row is not in the table, yet.
array_agg() has no role in this.
Would work with a trigger AFTER INSERT ON bdtravaux.operation_poly. But if id_oper is from the same inserted row, you can just simplify to:
SELECT array_to_string(array_agg(DISTINCT oper.operateurs),'; ')
INTO varoperateur
FROM bdtravaux.join_operateurs oper
WHERE oper.id_joinop = NEW.id_oper;
And keep the BEFORE trigger.
The whole function might be simpler, can probably done with a single query.

How to set a composite type column using dynamic sql in trigger procedure

I have a trigger function that is called by several tables when COLUMN A is updated, so that COLUMN B can be updated based on value from a different function. (More complicated to explain than it really is). The trigger function takes in col_a and col_b since they are different for the different tables.
IF needs_updated THEN
sql = format('($1).%2$s = dbo.foo(($1).%1$s); ', col_a, col_b);
EXECUTE sql USING NEW;
END IF;
When I try to run the above, the format produces this sql:
($1).NameText = dbo.foo(($1).Name);
When I execute the SQL with the USING I am expecting something like this to happen (which works when executed straight up without dynamic sql):
NEW.NameText = dbo.foo(NEW.Name);
Instead I get:
[42601] ERROR: syntax error at or near "$1"
How can I dynamically update the column on the record/composite type NEW?
This isn't going to work because NEW.NameText = dbo.foo(NEW.Name); isn't a correct sql query. And I cannot think of the way you could dynamically update variable attribute of NEW. My suggestion is to explicitly define behaviour for each of your tables:
IF TG_TABLE_SCHEMA = 'my_schema' THEN
IF TG_TABLE_NAME = 'my_table_1' THEN
NEW.a1 = foo(NEW.b1);
ELSE IF TG_TABLE_NAME = 'my_table_2' THEN
NEW.a2 = foo(NEW.b2);
... etc ...
END IF;
END IF;
First: This is a giant pain in plpgsql. So my best recommendation is to do this in some other PL, such as plpythonu or plperl. Doing this in either of those would be trivial. Even if you don't want to do the whole trigger in another PL, you could still do something like:
v_new RECORD;
BEGIN
v_new := plperl_function(NEW, column_a...)
The key to doing this in plpgsql is creating a CTE that has what you need in it:
c_new_old CONSTANT text := format(
'WITH
NEW AS (SELECT (r).* FROM (SELECT ($1)::%1$s r) s)
, OLD AS (SELECT (r).* FROM (SELECT ($2)::%1$s r) s
'
, TG_RELID::regclass
);
You will also need to define a v_new that is a plain record. You could then do something like:
-- Replace 2nd field in NEW with a new value
sql := c_new_old || $$SELECT row(NEW.a, $3, NEW.c) FROM NEW$$
EXECUTE sql INTO v_new USING NEW, OLD, new_value;

PL/SQL SELECT INTO alternative

I have few SELECT statements that are made by joining multiple tables.
Each select is returning single row but from 10 to 20 fields in that row. What is the easiest way to store that data for later use?
I would like to avoid creating 50 variables and writing select into statements, using cursors and loop for single row is not the smartest idea from what i read around.
Is there a good way to do this?
Stupid example so you can get general idea
SELECT t1.field1
, t1.field2
, t1.field3
, t1.field4
, t2.field5
, t2.field6
, t2.field7
, t3.field8
FROM table1 t1
JOIN table2 t2 ON something
JOIN table3 t3 ON something
Sorry for errors in my english and thanks in advance
Create a view from your select statement. Thereafter can reference a record by using a single variable of type <viewname>%ROWTYPE.
Another option would be to wrap the select in an implicit cursor loop:
DECLARE
strvar VARCHAR2(400); -- demo purpose only
BEGIN
-- ...
FOR i IN (
-- ... here goesyour select statement ...
) LOOP
strvar := i.field1 || i.field2; -- ... whatever
END LOOP;
-- ...
END;
-- ...
Still another option is the declaration of a record type and a record variable:
DECLARE
TYPE tRec IS RECORD (
field1 table1.field1%TYPE
, field2 table1.field2%TYPE
, field3 table1.field3%TYPE
, field4 table1.field4%TYPE
, field5 table2.field5%TYPE
, field6 table2.field6%TYPE
, field7 table2.field7%TYPE
, field8 table3.field8%TYPE
)
r tRec;
BEGIN
-- ...
SELECT --...
INTO r
FROM --...
;
-- ...
END;
-- ...
Every time you make a select you will have a cursor - there's no escape from that.
Views and explicit cursors are a way to reuse select statements. Which one is better option depends on the case.
rowtype-attribute is handy way to create records automatically based on tables/views/cursors. I think this is the closest to your requirement what PL/SQL can offer.
create or replace package so51 is
cursor cursor1 is -- an example single row query
select
dual.* -- all table columns included
,'Y' as str
,rownum as num
,sysdate as date_
from dual
;
function get_data return cursor1%rowtype;
end;
/
show errors
create or replace package body so51 is
-- function never changes when cursor1 changes
function get_data return cursor1%rowtype is
v_data cursor1%rowtype;
begin
open cursor1;
fetch cursor1 into v_data;
close cursor1;
return v_data;
end;
end;
/
show errors
declare
v_data constant so51.cursor1%rowtype := so51.get_data;
begin
-- use only the data you need
dbms_output.put_line('v_data.dummy = ' || v_data.dummy);
dbms_output.put_line('v_data.str = ' || v_data.str);
dbms_output.put_line('v_data.num = ' || v_data.num);
dbms_output.put_line('v_data.date_ = ' || v_data.date_);
end;
/
Example run
SQL> #so51.sql
Package created.
No errors.
Package body created.
No errors.
v_data.dummy = X
v_data.str = Y
v_data.num = 1
v_data.date_ = 2015-11-23 09:42:02
PL/SQL procedure successfully completed.
SQL>

How to use SIMILAR TO with variables

I have a function with a SELECT using a SIMILAR TO expression with a variable and I don't know how to do it:
DECLARE pckg_data cl_data;
DECLARE contacts contacts_reg%ROWTYPE;
DECLARE sim_name varchar;
BEGIN
SELECT client_reg._name,
client_reg.last_name,
client_reg.id_card,
client_reg.address
INTO pckg_data
FROM client_reg WHERE(client_reg._name = (cl_name ||' '|| cl_lastname));
RETURN NEXT pckg_data;
SELECT ('%'||cl_name || ' ' || cl_lastname ||'%') INTO sim_name;
FOR contacts IN SELECT contacts_reg.id
FROM contacts_reg, contactscli_asc, client_reg
WHERE(contacts_reg._name SIMILAR TO sim_name) LOOP
SELECT client_reg._name, client_reg.last_name, client_reg.id_card,
client_reg.address, client_reg.id
INTO pckg_data
FROM client_reg, contactscli_asc WHERE(contactscli_asc.contact = contacts.id
AND client_reg.id = contactscli_asc.client);
END LOOP;
END;
Your query that feeds the loop has CROSS JOIN over three (!) tables. I removed the last two on the notion that they are not needed. One of them is repeated in the body of the loop. Also consider #kgrittn's note on CROSS JOIN.
In the body of the loop you select data into a variable repeatedly, which does nothing. I assume you want to return those rows - that's what my edited version does, anyway.
I rewrote the LOOP construct with a simple SELECT with RETURN QUERY, because that's much faster and simpler.
Actually, I rewrote everything in a way that would make sense. What you presented is still incomplete (missing function header) and syntactically and logically a mess.
This is an educated guess, no more:
CREATE FUNCTION very_secret_function_name(cl_name varchar, cl_lastname varchar)
RETURNS TABLE (name varchar, last_name varchar,
id_card int, address varchar, id int)
LANGUAGE plpgsql AS
$func$
DECLARE
_sim_name varchar := (cl_name ||' '|| cl_lastname);
BEGIN
RETURN QUERY
SELECT c._name, c.last_name, c.id_card, c.address, NULL::int
-- added NULL for an id to match the second call
FROM client_reg c
WHERE c._name = _sim_name;
RETURN QUERY
SELECT c._name, c.last_name, c.id_card, c.address, r.id
FROM client_reg c
JOIN contactscli_asc a ON a.client = c.id
JOIN contacts_reg r ON r.id = a.contact
WHERE r._name LIKE ('%' || _sim_name || '%');
END
$func$;
Else, consider the features used.
Some advise:
You can assign a variable at declaration time.
The keyword DECLARE is only needed once.
Use table aliases to make your code easier to read.
You don't have to enclose the WHERE clause in parenthesis.
Most likely you don't need SIMILAR TO and LIKE does the job faster. I never use SIMILAR TO. LIKE or regular expressions (~) do a better job:
Pattern matching with LIKE, SIMILAR TO or regular expressions in PostgreSQL

eliminate duplicate array values in postgres

I have an array of type bigint, how can I remove the duplicate values in that array?
Ex: array[1234, 5343, 6353, 1234, 1234]
I should get array[1234, 5343, 6353, ...]
I tested out the example SELECT uniq(sort('{1,2,3,2,1}'::int[])) in the postgres manual but it is not working.
I faced the same. But an array in my case is created via array_agg function. And fortunately it allows to aggregate DISTINCT values, like:
array_agg(DISTINCT value)
This works for me.
The sort(int[]) and uniq(int[]) functions are provided by the intarray contrib module.
To enable its use, you must install the module.
If you don't want to use the intarray contrib module, or if you have to remove duplicates from arrays of different type, you have two other ways.
If you have at least PostgreSQL 8.4 you could take advantage of unnest(anyarray) function
SELECT ARRAY(SELECT DISTINCT UNNEST('{1,2,3,2,1}'::int[]) ORDER BY 1);
?column?
----------
{1,2,3}
(1 row)
Alternatively you could create your own function to do this
CREATE OR REPLACE FUNCTION array_sort_unique (ANYARRAY) RETURNS ANYARRAY
LANGUAGE SQL
AS $body$
SELECT ARRAY(
SELECT DISTINCT $1[s.i]
FROM generate_series(array_lower($1,1), array_upper($1,1)) AS s(i)
ORDER BY 1
);
$body$;
Here is a sample invocation:
SELECT array_sort_unique('{1,2,3,2,1}'::int[]);
array_sort_unique
-------------------
{1,2,3}
(1 row)
... Where the statandard libraries (?) for this kind of array_X utility??
Try to search... See some but no standard:
postgres.cz/wiki/Array_based_functions: good reference!
JDBurnZ/postgresql-anyarray, good initiative but needs some collaboration to enhance.
wiki.postgresql.org/Snippets, frustrated initiative, but "offcial wiki", needs some collaboration to enhance.
MADlib: good! .... but it is an elephant, not an "pure SQL snippets lib".
Simplest and faster array_distinct() snippet-lib function
Here the simplest and perhaps faster implementation for array_unique() or array_distinct():
CREATE FUNCTION array_distinct(anyarray) RETURNS anyarray AS $f$
SELECT array_agg(DISTINCT x) FROM unnest($1) t(x);
$f$ LANGUAGE SQL IMMUTABLE;
NOTE: it works as expected with any datatype, except with array of arrays,
SELECT array_distinct( array[3,3,8,2,6,6,2,3,4,1,1,6,2,2,3,99] ),
array_distinct( array['3','3','hello','hello','bye'] ),
array_distinct( array[array[3,3],array[3,3],array[3,3],array[5,6]] );
-- "{1,2,3,4,6,8,99}", "{3,bye,hello}", "{3,5,6}"
the "side effect" is to explode all arrays in a set of elements.
PS: with JSONB arrays works fine,
SELECT array_distinct( array['[3,3]'::JSONB, '[3,3]'::JSONB, '[5,6]'::JSONB] );
-- "{"[3, 3]","[5, 6]"}"
Edit: more complex but useful, a "drop nulls" parameter
CREATE FUNCTION array_distinct(
anyarray, -- input array
boolean DEFAULT false -- flag to ignore nulls
) RETURNS anyarray AS $f$
SELECT array_agg(DISTINCT x)
FROM unnest($1) t(x)
WHERE CASE WHEN $2 THEN x IS NOT NULL ELSE true END;
$f$ LANGUAGE SQL IMMUTABLE;
Using DISTINCT implicitly sorts the array. If the relative order of the array elements needs to be preserved while removing duplicates, the function can be designed like the following: (should work from 9.4 onwards)
CREATE OR REPLACE FUNCTION array_uniq_stable(anyarray) RETURNS anyarray AS
$body$
SELECT
array_agg(distinct_value ORDER BY first_index)
FROM
(SELECT
value AS distinct_value,
min(index) AS first_index
FROM
unnest($1) WITH ORDINALITY AS input(value, index)
GROUP BY
value
) AS unique_input
;
$body$
LANGUAGE 'sql' IMMUTABLE STRICT;
I have assembled a set of stored procedures (functions) to combat PostgreSQL's lack of array handling coined anyarray. These functions are designed to work across any array data-type, not just integers as intarray does: https://www.github.com/JDBurnZ/anyarray
In your case, all you'd really need is anyarray_uniq.sql. Copy & paste the contents of that file into a PostgreSQL query and execute it to add the function. If you need array sorting as well, also add anyarray_sort.sql.
From there, you can peform a simple query as follows:
SELECT ANYARRAY_UNIQ(ARRAY[1234,5343,6353,1234,1234])
Returns something similar to: ARRAY[1234, 6353, 5343]
Or if you require sorting:
SELECT ANYARRAY_SORT(ANYARRAY_UNIQ(ARRAY[1234,5343,6353,1234,1234]))
Return exactly: ARRAY[1234, 5343, 6353]
Here's the "inline" way:
SELECT 1 AS anycolumn, (
SELECT array_agg(c1)
FROM (
SELECT DISTINCT c1
FROM (
SELECT unnest(ARRAY[1234,5343,6353,1234,1234]) AS c1
) AS t1
) AS t2
) AS the_array;
First we create a set from array, then we select only distinct entries, and then aggregate it back into array.
In a single query i did this:
SELECT (select array_agg(distinct val) from ( select unnest(:array_column) as val ) as u ) FROM :your_table;
For people like me who still have to deal with postgres 8.2, this recursive function can eliminate duplicates without altering the sorting of the array
CREATE OR REPLACE FUNCTION my_array_uniq(bigint[])
RETURNS bigint[] AS
$BODY$
DECLARE
n integer;
BEGIN
-- number of elements in the array
n = replace(split_part(array_dims($1),':',2),']','')::int;
IF n > 1 THEN
-- test if the last item belongs to the rest of the array
IF ($1)[1:n-1] #> ($1)[n:n] THEN
-- returns the result of the same function on the rest of the array
return my_array_uniq($1[1:n-1]);
ELSE
-- returns the result of the same function on the rest of the array plus the last element
return my_array_uniq($1[1:n-1]) || $1[n:n];
END IF;
ELSE
-- if array has only one item, returns the array
return $1;
END IF;
END;
$BODY$
LANGUAGE 'plpgsql' VOLATILE;
for exemple :
select my_array_uniq(array[3,3,8,2,6,6,2,3,4,1,1,6,2,2,3,99]);
will give
{3,8,2,6,4,1,99}