I have the following plpgsql procedure;
DECLARE
_r record;
point varchar[] := '{}';
i int := 0;
BEGIN
FOR _r IN EXECUTE ' SELECT a.'|| quote_ident(column) || ' AS point,
FROM ' || quote_ident (table) ||' AS a'
LOOP
point[i] = _r;
i = i+1;
END LOOP;
RETURN 'OK';
END;
Which its main objective is to traverse a table and store each value of the row in an array. I am still new to plpgsql. Can anyone point out is the error as it is giving me the following error; enter image description here
2 Answers 2
This is the complete syntax (note that I renamed the parameter column to col_name as column is reserved word. The same goes for table)
create or replace function foo(col_name text, table_name text)
returns text
as
$body$
DECLARE
_r record;
point character varying[] := '{}';
i int := 0;
BEGIN
FOR _r IN EXECUTE 'SELECT a.'|| quote_ident(col_name) || ' AS pt, FROM ' || quote_ident (table_name) ||' AS a'
loop
point[i] = _r;
i = i+1;
END LOOP;
RETURN 'OK';
END;
$body$
language plpgsql;
Although to be honest: I fail so see what you are trying to achieve here.
3 Comments
@a_horse fixes most of the crippling problems with your failed attempt.
However, nobody should use this. The following step-by-step instructions should lead to a sane implementation with modern PostgreSQL.
Phase 1: Remove errors and mischief
Remove the comma after the
SELECTlist to fix the syntax error.You start your array with
0, while the default is to start with1. Only do this if you need to do it. Leads to unexpected results if you operate witharray_upper()et al. Start with1instead.Change
RETURNtype tovarchar[]to return the assembled array and make this demo useful.
What we have so far:
CREATE OR REPLACE FUNCTION foo(tbl varchar, col varchar)
RETURNS varchar[] LANGUAGE plpgsql AS
$BODY$
DECLARE
_r record;
points varchar[] := '{}';
i int := 0;
BEGIN
FOR _r IN
EXECUTE 'SELECT a.'|| quote_ident(col) || ' AS pt
FROM ' || quote_ident (tbl) ||' AS a'
LOOP
i = i + 1; -- reversed order to make array start with 1
points[i] = _r;
END LOOP;
RETURN points;
END;
$BODY$;
Phase 2: Remove cruft, make it useful
Use
textinstead ofcharacter varying/varcharfor simplicity. Either works, though.You are selecting a single column, but use a variable of type
record. This way a whole record is being coerced totext, which includes surrounding parenthesis. Hardly makes any sense. Use atextvariable instead. Works for any column if you explicitly cast totext(::text). Any type can be cast totext.There is no point in initializing the variable
point. It can start asNULLhere.Table and column aliases inside
EXECUTEare of no use in this case. Dynamically executed SQL has its own scope!.No semicolon (
;) needed after finalENDin a plpgsql function.It's simpler to just append each value to the array with
||.
Almost sane:
CREATE OR REPLACE FUNCTION foo1(tbl text, col text)
RETURNS text[] LANGUAGE plpgsql AS
$func$
DECLARE
point text;
points text[];
BEGIN
FOR point IN
EXECUTE 'SELECT '|| quote_ident(col) || '::text FROM ' || quote_ident(tbl)
LOOP
points = points || point;
END LOOP;
RETURN points;
END
$func$;
Phase 3: Make it shine in modern PL/pgSQL
If you pass a table name as
text, you create an ambiguous situation. You can prevent SQLi just fine withformat()orquote_ident(), but this will fail with tables outside yoursearch_path.
Then you need to add schema-qualification, which creates an ambiguous value. 'x.y' could stand for the table name "x.y" or the schema-qualified table name "x"."y". You can't pass "x"."y" since that will be escaped into """x"".""y""". You'd need to either use an additional parameter for the schema name or one parameter of typeregclassregclassis automatically quoted as need when coerced totextand is the elegant solution here.The new
format()is simpler than multiple (or even a single)quote_ident()call.You did not specify any order.
SELECTreturns rows in arbitrary order withoutORDER BY. This may seem stable, since the result is generally reproducible as long as the underlying table doesn't change. But that's 100% unreliable. You probably want to add some kind ofORDER BY.Finally, you don't need to loop at all. Use a plain
SELECTwith an Array constructor.Use an
OUTparameter to further simplify the code
Proper solution:
CREATE OR REPLACE FUNCTION f_arr(tbl regclass, col text, OUT arr text[])
LANGUAGE plpgsql AS
$func$
BEGIN
EXECUTE format('SELECT ARRAY(SELECT %I::text FROM %s ORDER BY 1)', col, tbl)
INTO arr;
END
$func$;
Call:
SELECT f_arr('myschema.mytbl', 'mycol');
create functionpart. And you need to quote the body of the function, e.g. using dollar-quoting. Please read the manual for details.CREATE [OR REPLACE] FUNCTIONblock isn't being sent. Maybe look at the underlying query logs?loopandend loopto define the scope of theforloop. Also you can't use table or column names dynamically as you do.