Postgres - Use table name (passed in parameter) in function body - postgresql

I am a newbie wrt functions and I am struggling with using the name of a table in the function body. I get an error "SQL Error [42703]: ERROR: column "tname" does not exist" when I call the function using
select "JsonToView"('data_import.import_360xero_report');
My code is below
create or replace
function data_import."JsonToView"(tname text) returns numeric
language plpgsql
as $function$
begin
do
$$
declare
l_keys text;
begin
drop view if exists v_json_view cascade;
select
string_agg(distinct format('import_data ->> %L as %I', jkey, jkey), ', ')
into
l_keys
from
import_360xero_report,
json_object_keys(import_data) as t(jkey);
execute 'create view v_json_view as select ' || l_keys || ' from ' || tname;
end;
$$;
return 0;
end $function$ ;
I have modified the code and the second create view query works with the table name but the first one does not.
Below if my modified code
create or replace
function data_import."JsonToView"(tname text) returns numeric
language plpgsql
as $function$
declare
l_keys text;
begin
drop view if exists v_json_view cascade;
execute $a$select
string_agg(distinct format('import_data ->> %L as %I', jkey, jkey), ', ')
into
l_keys
from $a$ ||
tname || $b$,
json_object_keys(import_data) as t(jkey)$b$;
execute 'create view v_json_view as select ' || l_keys || ' from ' || tname;
return 0;
end $function$ ;
The error I am getting is
SQL Error [0A000]: ERROR: EXECUTE of SELECT ... INTO is not implemented
Hint: You might want to use EXECUTE ... INTO or EXECUTE CREATE TABLE ... AS instead.
Where: PL/pgSQL function "JsonToView"(text) line 10 at EXECUTE

The problem is the superfluous nested DO statement.
The variable tname exists only in the scope of the function, not in the nested DO statement. DO is an SQL statement, not a PL/pgSQL statement, and there are no variables in SQL. Also, DO does not allow parameters.
Get rid of the DO and you will be fine.

Related

Using NEW.* inside EXECUTE regarding psql

I checked all related questions on SO but none helped in my case.
I have 2 loops(outside for the tables and inside for the columns). Tables are represented by 'r', and columns by 'm'. While being inside the 'm' loop which is supposed to send column values to the to-be-created trigger function. When I try to use 'NEW.m' (with trying many different formatting attempts) compiler always gives error.
Can you kindly advice on it please? Br
FOR r IN SELECT table_name FROM information_schema.tables LOOP
FOR m IN SELECT column_name FROM information_schema.columns WHERE (table_name = r.table_name ) LOOP
function_name := 'dictionary_functions_foreach_trigger';
EXECUTE format('CREATE OR REPLACE FUNCTION %s()
RETURNS trigger AS
$BODY$
DECLARE
BEGIN
IF NEW.m IS NOT NULL AND NEW.m IN (SELECT key FROM tableX.tableX_key)
THEN RETURN NEW;
END IF;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
ALTER FUNCTION %s()
OWNER TO mydb;',function_name, function_name);
EXECUTE 'CREATE TRIGGER ' || function_name || ' BEFORE INSERT OR UPDATE ON ' || belonging_to_schema || '.' || r.table_name || ' FOR EACH ROW EXECUTE PROCEDURE ' || function_name || '();';
----Trigger Functions after edit-
EXECUTE format(
'CREATE OR REPLACE FUNCTION %s()
RETURNS trigger AS
$BODY$
DECLARE
insideIs text := %s ;
BEGIN
FOR %s IN 0..(TG_NARGS-1) LOOP
IF %I= TG_ARGV[%s]
THEN insideIs := %s ;
END IF;
END LOOP;
IF NEW.%I IS NOT NULL AND (insideIs =%s) AND NEW.%I IN (SELECT key FROM tableX.tableX_key)
THEN RETURN NEW;
ELSE RETURN OLD;
END IF;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
ALTER FUNCTION %s()
OWNER TO mydb;' , function_name, 'notInside', 'i' , m.column_name, 'i' , 'ok', m.column_name, 'ok', m.column_name ,function_name);
You need to use another placeholder for the column name, they way you have written it, the column name "m" is hardcoded in the function.
You also don't really need the outer loop, as the table_name is also available in information_schema.columns.
Your trigger would also fail with a runtime error if the condition is not true as you don't have a return in that case. If you want to abort the statement, use return null;
You should also use format() for the create trigger statement.
FOR m IN SELECT table_schema, table_name, column_name
FROM information_schema.columns
WHERE table_name in (...)
LOOP
function_name := 'dictionary_functions_foreach_trigger';
EXECUTE format('CREATE OR REPLACE FUNCTION %I()
RETURNS trigger AS
$BODY$
DECLARE
BEGIN
IF NEW.%I IS NOT NULL AND NEW.%I IN (SELECT key FROM tableX.tableX_key) THEN
RETURN NEW;
END IF;
RETURN null; --<< you need some kind of return here!
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
ALTER FUNCTION %s()
OWNER TO mydb;', function_name, m.column_name, m.column_name, function_name, function_name);
EXECUTE format('CREATE TRIGGER %I BEFORE INSERT OR UPDATE ON %I.%I FOR EACH ROW EXECUTE PROCEDURE %I()',
function_name, m.table_schema, m.table_name, function_name);
END LOOP;
Online example

Is it possible to nest an EXECUTE statement as a source recordset in PostgreSQL?

This is the case of a dynamic _sql select passed to a function that tries to execute the query and return a JSON array with the result.
create or replace function jlist_objects_bysql (
_sql varchar
)
returns json
as $$
select
json_agg (t)
from (
execute _sql
) as t;
$$ language sql;
The validation error in PostgreSQL 12 is
psql:objects_procedures.sql:874: ERROR: syntax error at or near "t"
LINE 8: from execute _sql t;
Dynamic SQL does not work with the language sql but rather with language plpgsql.
Then, you can enlarge the dynamic query
create or replace function jlist_objects_bysql (
_sql varchar
)
returns json
as $$
declare
output json;
BEGIN
execute 'select json_agg (t) from ( ' || _sql || '
) as t;'
INTO output;
return output;
END
$$ language plpgsql;
select jlist_objects_bysql('select * from test');

How to convert a PL/PgSQL procedure into a dynamic one?

I am trying to write a plpgsql procedure to perform spatial tiling of a postGIS table. I can perform the operation successfully using the following procedure in which the table names are hardcoded. The procedure loops through the tiles in tile_table and for each tile clips the area_table and inserts it into split_table.
CREATE OR REPLACE PROCEDURE splitbytile()
AS $$
DECLARE
tile RECORD;
BEGIN
FOR tile IN
SELECT tid, geom FROM test_tiles ORDER BY tid
LOOP
INSERT INTO split_table (id, areaname, ttid, geom)
SELECT id, areaname, tile.tid,
CASE WHEN st_within(base.geom, tile.geom) THEN st_multi(base.geom)
ELSE st_multi(st_intersection(base.geom, tile.geom)) END as geom
FROM area_table as base
WHERE st_intersects(base.geom, tile.geom);
COMMIT;
END LOOP;
END;
$$ LANGUAGE 'plpgsql';
Having tested this successfully, now I need to convert it to a dynamic procedure where I can provide the table names as parameters. I tried the following partial conversion, using format() for inside of loop:
CREATE OR REPLACE PROCEDURE splitbytile(in_table text, grid_table text, split_table text)
AS $$
DECLARE
tile RECORD;
BEGIN
FOR tile IN
EXECUTE format('SELECT tid, geom FROM %I ORDER BY tid', grid_table)
LOOP
EXECUTE
FORMAT(
'INSERT INTO %1$I (id, areaname, ttid, geom)
SELECT id, areaname, tile.tid,
CASE WHEN st_within(base.geom, tile.geom) THEN st_multi(base.geom)
ELSE st_multi(st_intersection(base.geom, tile.geom)) END as geom
FROM %2$I as base
WHERE st_intersects(base.geom, tile.geom)', split_table, in_table
);
COMMIT;
END LOOP;
END;
$$ LANGUAGE 'plpgsql';
But it throws an error
missing FROM-clause entry for table "tile"
So, how can I convert the procedure to a dynamic one? More specifically, how can I use the record data type (tile) returned by the for loop inside the loop? Note that it works when format is not used.
You can use EXECUTE ... USING to supply parameters to a dynamic query:
EXECUTE
format(
'SELECT r FROM %I WHERE c = $1.val',
table_name
)
INTO result_var
USING record_var;
The first argument to USING will be used for $1, the second for $2 and so on.
See the documentation for details.
Personally I use somehow different way to create dynamic functions. By concatination and execute function. You can also do like this.
CREATE OR REPLACE FUNCTION splitbytile()
RETURNS void AS $$
declare
result1 text;
table_name text := 'test_tiles';
msi text := '+7 9912 231';
msi text := 'Hello world';
code text := 'code_name';
_operator_id integer := 2;
begin
query1 := 'SELECT msisdn from ' || table_name || ' where msisdn = ''' || msi::text ||''';';
query2 := 'INSERT INTO ' || table_name || '(msisdn,usage,body,pr_code,status,sent_date,code_type,operator_id)
VALUES( ''' || msi::text || ''',' || true || ',''' || _body::text || ''',''' || code::text || ''',' || false || ',''' || time_now || ''',' || kod_type || ',' || _operator_id ||');';
execute query1 into result1;
execute query2;
END;
$function$
You just make your query as text then anywhere you want you can execute it. Maybe by checking result1 value inside If statement or smth like that.

How to declare bound cursor with customized column name in pl/pgsql

I would like to use cursor in a function with the table name as a function variable, a simple example would be a select query through cursor.
From the documentation of PostgreSQL I found that I can use
Declare curs3 CURSOR (key integer) FOR SELECT * FROM tenk1 WHERE unique1 = key;
But when I input
declare curs1 cursor (key integer) for execute 'select ' || quote_ident(colname) || ' from ' || quote_ident(tablename);
It returns ERROR: syntax error at or near "'select '".
On the other hand, if I write the function with refcursor as follows:
CREATE or replace FUNCTION cursor_hw(colname text,tablename text) RETURNS setof text AS $$
declare curs1 refcursor;
BEGIN
open curs1 for execute 'select ' || quote_ident(colname) || ' from ' || quote_ident(tablename);
for x in curs1 loop
return next x;
end loop;
END; $$ LANGUAGE plpgsql;
It will return [42601] ERROR: cursor FOR loop must use a bound cursor variable.
Any help would be appreciated, thanks a lot!
You may prefer a simple FOR record_variable IN EXECUTE <query> instead of OPEN FETCH for dynamic SQL.
CREATE or replace FUNCTION cursor_hw(colname text,tablename text)
RETURNS setof text AS
$$
DECLARE
x RECORD;
BEGIN
FOR x IN execute 'select ' || quote_ident(colname) || ' from '
|| quote_ident(tablename)
LOOP
IF x.first_name like 'D%' THEN
RETURN NEXT x;
END IF;
END LOOP;
END;
$$ LANGUAGE plpgsql;
execution
knayak=# select cursor_hw('first_name','employees');
cursor_hw
-----------
Donald
Douglas
David
Diana
Daniel
Den
David
Danielle
David
(9 rows)

SELECTing commands into a temp table to EXECUTE later in PostgreSQL

For some fancy database maintenance for my developer database I'd like to be able to use queries to generate commands to alter the database. The thing is: I'm a complete greenhorn to PostgreSQL. I've made my attempt but have failed colorfully.
So in the end, I would like to have a table with a single column and each row would be a command (or group of commands, depending on the case) that I would think would look something like this...
DO $$
DECLARE
command_entry RECORD;
BEGIN
FOR command_entry IN SELECT * FROM list_of_commands
LOOP
EXECUTE command_entry;
END LOOP;
END;
$$;
Where the table list_of_commands could be populated with something like the following (which in this example would remove all tables from the public schema)...
CREATE TEMP TABLE list_of_commands AS
SELECT 'drop table if exists "' || tablename || '" cascade;'
FROM pg_tables
WHERE schemaname = 'public';
However, with this I get the following error...
ERROR: syntax error at or near ""drop table if exists ""dummy_table"" cascade;""
LINE 1: ("drop table if exists ""dummy_table"" cascade;")
I assume this is a matter of escaping characters, but I'm not entirely sure how to fit that into either A) the population of the table or B) the execution of each row. Does anyone know what I could do to achieve the desired result?
The command_entry variable is of type record while the EXECUTE command expects a string. What is apparently happening is that PostgreSQL turns the record into a double-quoted string, but that messes up your command. Also, your temp table does not use a column name, making things a bit awkward to work with (the column name becomes ?column?), so change both as follows:
CREATE TEMP TABLE list_of_commands AS
SELECT 'drop table if exists public.' || quote_ident(tablename) || ' cascade' AS cmd
FROM pg_tables
WHERE schemaname = 'public';
DO $$
DECLARE
command_entry varchar;
BEGIN
FOR command_entry IN SELECT cmd FROM list_of_commands
LOOP
EXECUTE command_entry;
END LOOP;
END;
$$;
But seeing that you do all of this at session level (temp table, anonymous code block), why not write a stored procedure that performs all of this housekeeping when you are ready to do spring cleaning?
CREATE FUNCTION cleanup() RETURNS void AS $$
BEGIN
FOR tbl IN SELECT tablename FROM pg_tables WHERE schemaname = 'public'
LOOP
EXECUTE 'DROP TABLE IF EXISTS ' || quote_ident(tbl) || ' CASCADE';
END LOOP;
-- More housekeeping jobs
END;
$$ LANGUAGE plpgsql;
This saves a lot of typing: SELECT cleanup();. Any other housekeeping jobs you have you simply add to the stored procedure.
I had trouble with Patrick's answers, so here is an updated version for postgreSQL 10.
CREATE FUNCTION droptables(sn varchar) RETURNS void AS $$
DECLARE
tbl varchar;
BEGIN
FOR tbl IN SELECT tablename FROM pg_tables WHERE schemaname = sn
LOOP
EXECUTE 'DROP TABLE IF EXISTS ' || quote_ident(tbl) || ' CASCADE';
END LOOP;
END;
$$ LANGUAGE plpgsql;
And then "SELECT droptables('public');".