Reusing a declared variable in postgres function - postgresql

I am writing a postgresql function and my construct is as follows:
CREATE OR REPLACE FUNCTION function_name (argument_list) RETURNS INTEGER []
AS $$
DECLARE
--along with other declarations
_tablename text;
BEGIN
-- dynamically construct the intermediate _tablename which gets
-- populated
-- Now I want to use this _tablename in other queries like :
-- use it in the select from _tablename loop
-- construct array by selecting a column from this table
-- and return that array
END
How should I do this? I want to reuse the declared variable name in my further queries in the function.
My complete postgres function is as follows:
DROP FUNCTION get_value_histogram(BIGINT,BIGINT,BIGINT,INTEGER);
CREATE OR REPLACE FUNCTION get_value_histogram(customer_id BIGINT,
start_time BIGINT, end_time BIGINT, bucket_size INTEGER)
RETURNS INTEGER[] AS
$$
DECLARE
_tablename text;
_curr_timestamp BIGINT;
_var1 text;
_min_value INTEGER;
_max_value INTEGER;
_return_array INTEGER[];
BEGIN
-- create an intermediate table with the aggregation of the
-- required values. These values then will be passed to the
-- Histogram function.
_var1 := EXTRACT (EPOCH FROM now());
_var1 := replace(_var1, '.','_');
_tablename := 'thing_data_' || _var1;
EXECUTE 'CREATE TABLE ' || _tablename || ' (t_stamp BIGINT, sum_of_values INTEGER)';
--insert all the values in this intermediate table
EXECUTE ' INSERT INTO ' || _tablename || ' ( select t_stamp , sum(data) from thing_data td, collector_tb ct where td.thingname =
ct.collector_name and td.t_stamp BETWEEN ' || quote_literal(start_time) || ' AND ' || quote_literal(end_time) || ' and
ct.type like ' || quote_literal('%outlet%') ||' AND customer_id = ' || customer_id || ' GROUP BY t_stamp)' ;
EXECUTE 'select width_bucket(sum_of_values,500, 1000 , 100), count(*) as cnt from ' || _tablename || ' GROUP BY 1 ORDER BY 1' ;
_return_array := array (select cnt from (select width_bucket(sum_of_values,500, 1000 , 100), count(*) as cnt from _tablename GROUP BY 1 ORDER BY 1));
EXECUTE 'DROP TABLE ' || _tablename;
RETURN _return_array;
END $$ LANGUAGE plpgsql;
When I run this, I get an error saying relation "_tablename" does not exist

just replace :
_return_array := array (select cnt from (select width_bucket(sum_of_values,500, 1000 , 100), count(*) as cnt from _tablename GROUP BY 1 ORDER BY 1) a);
by :
EXECUTE 'select array (select cnt from (select width_bucket(sum_of_values,500, 1000 , 100), count(*) as cnt from '|| _tablename ||' GROUP BY 1 ORDER BY 1) a)' into _return_array;

I assume the error is in the last part:
_return_array := array (select cnt from (select width_bucket(sum_of_values,500, 1000 , 100), count(*) as cnt
from _tablename GROUP BY 1 ORDER BY 1));
Here you're using _tablename as an actual literal table name and not as a variable.

Related

How to use a Function Parameter in a Cursor that's incorporated with Dynamic SQL in Postgres Functions?

Created this Postgres Function which is working fine, but the actual requirement is to pass the input parameter in the function to the Cursor which uses the dynamic SQL as follows,
The below is the Function
CREATE OR REPLACE FUNCTION ssp2_pcat.find_shift_dates (date_to_find date)
RETURNS void
LANGUAGE 'plpgsql'
COST 100
VOLATILE
AS $BODY$
DECLARE
C1 CURSOR FOR
SELECT TABLE_NAME, 'SELECT COUNT(*) FROM ' || TABLE_NAME || ' WHERE ' ||
COLUMN_NAME || ' = '||
'CASE WHEN ' || COLUMN_NAME || ' LIKE ' || '''%START%'''||' THEN
date_to_find ELSE date_to_find-1 END;' SQL_TEXT
FROM (
SELECT TABLE_NAME, COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME IN (SELECT TABLE_NAME FROM RESET_DATES WHERE RESET_IT =
'Y') AND
UPPER(DATA_TYPE) = 'DATE'
AND (COLUMN_NAME LIKE '%START%' OR COLUMN_NAME LIKE '%END%')
AND (COLUMN_NAME NOT LIKE '%TEST%'
AND COLUMN_NAME NOT LIKE '%PCAT%'
AND COLUMN_NAME NOT LIKE '%ORDER%'
AND COLUMN_NAME NOT LIKE '%SEASON%'
AND COLUMN_NAME NOT LIKE '%_AT')
ORDER BY 1, 2) A;
END_COUNT INTEGER := 0;
START_COUNT INTEGER := 0;
TABLENAME VARCHAR(32) := 'ALFU';
l_start TIMESTAMP;
l_end TIMESTAMP;
Time_Taken VARCHAR(20);
BEGIN
l_start := clock_timestamp();
DELETE FROM SHIFT_DATES_COUNT;
FOR I IN C1 LOOP
IF I.TABLE_NAME <> TABLENAME THEN
INSERT INTO SHIFT_DATES_COUNT VALUES (TABLENAME, START_COUNT,
END_COUNT, current_timestamp::timestamp(0));
TABLENAME := I.TABLE_NAME;
END_COUNT := 0;
START_COUNT := 0;
END IF;
IF STRPOS(I.SQL_TEXT, 'END') > 0 THEN
EXECUTE I.SQL_TEXT INTO END_COUNT;
RAISE NOTICE '% ', ('END: ' || I.SQL_TEXT);
ELSE
EXECUTE I.SQL_TEXT INTO START_COUNT;
RAISE NOTICE '% ', ('START: ' || I.SQL_TEXT);
END IF;
END LOOP;
INSERT INTO SHIFT_DATES_COUNT VALUES (TABLENAME, START_COUNT, END_COUNT,
current_timestamp::timestamp(0));
RAISE NOTICE '% ', ('INSERT INTO SHIFT_DATES_COUNT Done...');
l_end := clock_timestamp();
Time_Taken := (l_end-l_start);
RAISE NOTICE '% ', ('FIND_SHIFT_DATES Took: ' || Time_Taken );
END;
$BODY$;
Please let me know how can I use the date_to_find input parameter in the Dynamic SQL in the Cursor in the above Function.
You can use unbound cursor, clause fetch to get data from cursor, and exit when not found to finish, like:
CREATE OR REPLACE FUNCTION example (p_name text) RETURNS void LANGUAGE 'plpgsql' AS $$
DECLARE
C1 refcursor;
res record;
BEGIN
OPEN c1 FOR EXECUTE 'SELECT * FROM pg_database WHERE datname like ''%'||p_name||'%''';
LOOP
FETCH c1 INTO res;
EXIT WHEN not found;
raise notice 'value datname: %',res.datname;
END LOOP;
CLOSE c1;
RETURN;
END; $$;
--in my case
select example ('test')
NOTICE: value datname: test
NOTICE: value datname: test_msmov
NOTICE: value datname: test_resources
NOTICE: value datname: test_load_table
NOTICE: value datname: test_resources2
Total query runtime: 63 msec
1 row retrieved.
You can use EXECUTE clause for open cursor, see the documentation of PostgreSQL
https://www.postgresql.org/docs/10/plpgsql-cursors.html#PLPGSQL-CURSOR-OPENING
Example:
OPEN curs1 FOR EXECUTE format('SELECT * FROM %I WHERE col1 = $1',tabname) USING keyvalue;

ERROR: missing FROM-clause entry for table "new"

I have a parent table layer_1_ and a number of child tables layer_1_points, layer_1_linestrings etc. which contain some geometry data. Each child table has its own geometry constraint. So, for example, layer_1_points has this constraint:
CONSTRAINT enforce_geotype_geom_geom CHECK (geometrytype(geom) = 'POINT'::text)
Whereas layer_1_linestrings table has this constraint:
CONSTRAINT enforce_geotype_geom_geom CHECK (geometrytype(geom) = 'LINESTRING'::text)
Many other layer tables have similar names: layer_2_, layer_3_, ..., layer_N_. And all of them have their own child tables. What I want to achive is that when a user inserts to a parent table (layer_N_), then this insert statement should be forwarded to a particular child table (layer_N_points etc.). So, for example, when I do:
INSERT INTO layer_1_ (geom) VALUES(ST_GeomFromText('POINT(0 0)', 3857))
I should actually insert to layer_1_points, because geom type is POINT. To achive all this I created this trigger function and the trigger itself:
CREATE OR REPLACE FUNCTION trigger_layer_insert()
RETURNS trigger AS
$$
DECLARE
var_geomtype text;
table_name text;
layer_id text := (TG_ARGV[0])::text;
BEGIN
var_geomtype := geometrytype(NEW.geom);
IF var_geomtype = 'POINT' THEN
table_name := (SELECT concat ('layer_', layer_id, '_points'));
ELSIF var_geomtype = 'MULTIPOINT' THEN
table_name := (SELECT concat ('layer_', layer_id, '_multipoints'));
ELSIF var_geomtype = 'LINESTRING' THEN
table_name := (SELECT concat ('layer_', layer_id, '_linestrings'));
ELSIF var_geomtype = 'MULTILINESTRING' THEN
table_name := (SELECT concat ('layer_', layer_id, '_multilinestrings'));
ELSIF var_geomtype = 'POLYGON' THEN
table_name := (SELECT concat ('layer_', layer_id, '_polygons'));
ELSIF var_geomtype = 'MULTIPOLYGON' THEN
table_name := (SELECT concat ('layer_', layer_id, '_multipolygons'));
END IF;
EXECUTE '
INSERT INTO ' || table_name || '
SELECT * FROM (SELECT NEW.*) AS t
';
RETURN NULL;
END;
$$
LANGUAGE 'plpgsql' VOLATILE;
CREATE TRIGGER trigger_layer_1_ BEFORE INSERT
ON layer_1_ FOR EACH ROW
EXECUTE PROCEDURE trigger_layer_insert(1);
However, when I do actual insert like:
INSERT INTO layer_1_ (geom) VALUES(ST_GeomFromText('POINT(0 0)', 3857))
I get an error message:
ERROR: missing FROM-clause entry for table "new"
LINE 3: SELECT * FROM (SELECT NEW.*) AS t
^
QUERY:
INSERT INTO layer_1_points
SELECT * FROM (SELECT NEW.*) AS t
So, what is wrong with SELECT NEW.* and how can I fix it? Thanks!
EDIT
I also tried this:
EXECUTE '
INSERT INTO ' || table_name || '
SELECT * FROM (SELECT NEW.*) AS t
' USING NEW;
But it has no effect.
When you execute something using PLPGSQL statement EXECUTE it runs in the different context so local variables is not visible there. To pass variable(s) the EXECUTE '<SQL script>' USING <variables list>; form is used:
EXECUTE 'insert into table(field1, field2) values ($1, $2)' USING var1, var2;
So the statement should be:
EXECUTE 'INSERT INTO ' || table_name || ' SELECT * FROM SELECT $1.*) AS t'
USING NEW;
But much more secure is using format function:
execute format('INSERT INTO %I SELECT * FROM SELECT $1.*) AS t', table_name)

Postgresql function return multiple select statements

Can any one of you tell me how to approach this:
CREATE OR REPLACE FUNCTION name()
RETURNS ????? AS
$func$
BEGIN
SELECT * FROM tbl_a a;
SELECT * FROM tbl_b b;
END
$func$ LANGUAGE plpgsql;
Both tables have different structures.
You can use cursors but I can hardly imagine why you need such a function.
CREATE OR REPLACE FUNCTION my_multiselect(refcursor, refcursor) RETURNS VOID AS
$func$
BEGIN
OPEN $1 FOR SELECT * FROM information_schema.routines;
OPEN $2 FOR SELECT * FROM information_schema.sequences;
END
$func$ LANGUAGE plpgsql;
BEGIN;
SELECT my_multiselect('first_cursor_to_routines', 'second_cursor_to_sequences');
FETCH ALL IN first_cursor_to_routines;
FETCH ALL IN second_cursor_to_sequences;
COMMIT;
I'm not really sure what you're doing with this, but it sounds like you just want to return a union of these distinct result sets. You can do this with a dynamic query. I'm using Postgres 9.4.
CREATE OR REPLACE FUNCTION make_query(IN p_tables text[])
RETURNS void AS
$BODY$
DECLARE
v_qry text;
v_cols text;
v_types text;
v_as text;
BEGIN
EXECUTE format('
WITH sub AS (
SELECT
table_name,
column_name,
data_type
FROM
information_schema.columns
WHERE
table_name = ANY(%L)
ORDER BY
table_name,
ordinal_position)
,sub2 AS(
SELECT
DISTINCT ON (column_name, data_type)
column_name || '' '' || data_type AS def
FROM
sub
)
SELECT
string_agg(def, '','')
FROM
sub2;
',
p_tables
) INTO v_types;
v_qry := '
CREATE OR REPLACE FUNCTION name()
RETURNS TABLE(' || v_types || ') AS
$func$';
FOR i IN 1..array_upper(p_tables, 1)
LOOP
v_as := 'tbl' || i;
EXECUTE format('
WITH sub AS (
SELECT
table_name,
column_name,
data_type
FROM
information_schema.columns
WHERE
table_name = ANY(%L)
ORDER BY
table_name,
ordinal_position)
,sub2 AS(
SELECT
DISTINCT ON (column_name, data_type)
CASE WHEN table_name = ''%I''
THEN %L || ''.'' || column_name
ELSE ''NULL::'' || data_type
END AS cols
FROM
sub
)
SELECT
string_agg(cols, '','')
FROM
sub2;
',
p_tables,
p_tables[i],
v_as
) INTO v_cols;
IF i > 1 THEN
v_qry := v_qry || '
UNION ALL';
END IF;
v_qry := v_qry || '
SELECT ' || v_cols || ' FROM ' || p_tables[i] || ' AS ' || v_as;
IF i = array_upper(p_tables, 1) THEN
v_qry := v_qry || ';';
END IF;
END LOOP;
v_qry := v_qry || '
$func$ LANGUAGE sql;
';
EXECUTE v_qry;
END;
$BODY$
LANGUAGE plpgsql VOLATILE;
Sorry it looks ugly here, but this formatting helps the final product look nicer. If you're shy about executing a dynamic query like this off the bat, just replace EXECUTE v_qry; with RAISE INFO 'v_qry: %', v_qry; and it will simply print the dynamic query out in a message without executing it, so you can review what it will do once executed.
Then execute make_query() with a list of tables you want to display like this:
SELECT make_query(ARRAY['tbl_a', 'tbl_b']);
The result is that you will now have a function called name() which you can call in order to see the results of both tables at the same time, with all the union details already sorted out:
SELECT * FROM name();

how can I insert data by declaring all fields (not *)?

Here's my insert query:
INSERT INTO listing_replica_child (
(
SELECT rtz_comma_list(column_name)
FROM information_schema.columns
WHERE table_name = 'listing'
)
)
VALUES (
(
SELECT (
(
SELECT rtz_comma_list(column_name)
FROM information_schema.columns
WHERE table_name = 'listing'
)
FROM listing
WHERE listing_id = 9656
)
)
);
I'm using postgres.
do $$
DECLARE rec TEXT;
BEGIN
SELECT 'insert into listing_replica_child (' || t.col || ') select * from listing WHERE listing_id = 9656 '
INTO rec
FROM (
SELECT string_agg(column_name, ',') col
FROM information_schema.columns
WHERE table_name = 'listing'
) t;
EXECUTE rec;
END;$$;
You can wrap this dynamic query into a function like this
CREATE OR replace FUNCTION insert_listing_replica_child (_listing_id INT)
RETURNS void AS $$
DECLARE rec TEXT;
BEGIN
SELECT 'insert into listing_replica_child (' || t.col || ') select * from listing WHERE listing_id = ' || _listing_id || ' '
INTO rec
FROM (
SELECT string_agg(column_name, ',') col
FROM information_schema.columns
WHERE table_name = 'listing'
) t;
EXECUTE rec;
END $$
LANGUAGE plpgsql
So if want to insert values from listing table with lissting_id=9656 into table listing_replica_child
just Call
select insert_listing_replica_child (9656)
The way to specify all destination fields is to omit the column list entirely.
Your attempt then becomes:
INSERT INTO listing_replica_child
SELECT * FROM listing
WHERE listing_id = 9656
without loss of intention: If the number and type of the fields of the tables differs with your attempt, execution will explode (as will this query).
Even though you have asked that * not be used, it is the simplest and best way to achieve the task.

Running PostgreSQL stored procedures in SQL console

I have following stored procedure
CREATE FUNCTION runMortalityModel(a_user_id integer) RETURNS integer AS $$
DECLARE
t1 RECORD;
t2 RECORD;
numberOfDeaths integer;
BEGIN
SELECT person.id personId, person.age, condprobmin, condprobmax, random() experiment
INTO t1
FROM person, mortality_cond_prob
WHERE (user_id = a_user_id) and
(person.age = mortality_cond_prob.age);
SELECT personId
INTO t2
FROM t1
WHERE (tmp.condprobmin <= experiment) and (experiment <= tmp.condprobmax);
SELECT COUNT(*)
INTO numberOfDeaths
FROM t2;
RAISE 'numberOfDeaths=%', numberOfDeaths;
EXECUTE
'DELETE '
|| 'FROM person '
|| 'WHERE person.id IN '
|| t2;
RETURN numberOfDeaths;
END
$$ LANGUAGE plpgsql;
When I try to run this stored procedure using
SELECT runMortalityModel(1);
I get the error Relation »t1« doesn't exist.
How can I fix it?
Update 1: Changed the stored procedure declaration to
CREATE OR REPLACE FUNCTION runMortalityModel(a_user_id integer) RETURNS integer AS $$
DECLARE
t1 RECORD;
t2 RECORD;
numberOfDeaths integer;
BEGIN
EXECUTE 'SELECT person.id personId, person.age, condprobmin, condprobmax, random() experiment '
|| 'FROM person, mortality_cond_prob '
|| 'WHERE (user_id = ' || a_user_id || ') and '
|| '(person.age = mortality_cond_prob.age)'
INTO t1;
EXECUTE 'SELECT personId '
|| 'FROM ' || t1
|| ' WHERE (tmp.condprobmin <= experiment) and (experiment <= tmp.condprobmax)'
INTO t2;
EXECUTE 'SELECT COUNT(*) '
|| 'FROM ' || t2
INTO numberOfDeaths;
RAISE 'numberOfDeaths=%', numberOfDeaths;
EXECUTE
'DELETE '
|| 'FROM person '
|| 'WHERE person.id IN '
|| t2;
RETURN numberOfDeaths;
END
$$ LANGUAGE plpgsql;
I see several issues with original code:
You're trying to use RECORD variable as a relation, you should do ... FROM (SELECT t1.*) s instead;
I see no point to select 1 record, then do a query on that record and then perform count(*), you will always have either 0 or 1 as a result.
You second version looks much better, go for it.
This one seems to work. If you have better ideas, please tell them.
CREATE FUNCTION runMortalityModel(a_user_id integer) RETURNS integer AS $$
DECLARE
t1 RECORD;
curRecord RECORD;
numberOfDeaths integer;
BEGIN
numberOfDeaths := 0;
FOR curRecord IN
SELECT person.id personId, condprobmin, condprobmax, random() experiment
FROM person, mortality_cond_prob
WHERE (user_id = a_user_id) and
(person.age = mortality_cond_prob.age)
LOOP
IF (curRecord.condprobmin <= curRecord.experiment) AND (curRecord.experiment <= curRecord.condprobmax) THEN
EXECUTE
'DELETE '
|| 'FROM person '
|| 'WHERE person.id = ' || curRecord.personId;
numberOfDeaths := numberOfDeaths + 1;
END IF;
END LOOP;
RETURN numberOfDeaths;
END
$$ LANGUAGE plpgsql;