Syntax error in SQL statement of PL/pgSQL function - sql

I have written a stored procedure with the help of the SO community. I have cobbled/hacked together the answers of various questions to write my function.
However, when I try to create my function in the db (PostgreSQL 8.4), I get the following errors:
ERROR: syntax error at or near "$4"
LINE 1: ...ank() OVER (ORDER BY $1 , $2 ) / $3 )::int as $4 , $1 ,...
^
AND
QUERY: SELECT ceil(rank() OVER (ORDER BY $1 , $2 ) / $3 )::int as $4 , $1 , $2 , $5 , $6 , $7 , $8 , $9 , $10 FROM foobar WHERE $2 BETWEEN $11 AND $12 AND $1 = ANY( $13 ) ORDER BY $1 , $2 , $4
CONTEXT: SQL statement in PL/PgSQL function "custom_group" near line 9
This is the code for the function I am trying to create:
CREATE OR REPLACE FUNCTION custom_group(start_date DATE, end_date DATE, grouping_period INTEGER, _ids int[] DEFAULT '{}')
RETURNS TABLE (
grp INTEGER,
id INTEGER,
entry_date DATE,
pop REAL,
hip REAL,
lop REAL,
pcl REAL,
vop BIGINT,
poi BIGINT) AS
$BODY$
BEGIN
IF _ids <> '{}'::int[] THEN -- excludes empty array and NULL
RETURN QUERY
SELECT ceil(rank() OVER (ORDER BY id, entry_date) / $3)::int as grp
,id, entry_date, pop, hip, lop, pcl, vop, poi
FROM foobar
WHERE entry_date BETWEEN start_date AND end_date AND id = ANY(_ids)
ORDER BY id, entry_date, grp ;
ELSE
RETURN QUERY
SELECT ceil(rank() OVER (ORDER BY id, entry_date) / $3)::int as grp
,id, entry_date, pop, hip, lop, pcl, vop, poi
FROM foobar
WHERE entry_date BETWEEN start_date AND end_date
ORDER BY id, entry_date, grp ;
END IF;
END;
$BODY$ LANGUAGE plpgsql;
Can anyone understand why I am getting these errors - and how do I fix them?

The error comes from a naming conflict.
The variable grp is defined implicitly by the RETURNS TABLE clause. In the function body, you try to use the same identifier as column alias, which conflicts.
Just use a different name for grp - the column alias will not be visible outside of the function anyway.
And table-qualify the other columns:
CREATE OR REPLACE FUNCTION custom_group(_start_date DATE
,_end_date DATE
,_grouping_period INTEGER,
,_ids int[] DEFAULT '{}')
RETURNS TABLE (grp int, id int, entry_date date, pop real, hip real,
lop real, pcl real, vop bigint, poi bigint) AS
$BODY$
BEGIN
IF _ids <> '{}'::int[] THEN -- excludes empty array and NULL
RETURN QUERY
SELECT ceil(rank() OVER (ORDER BY f.id, f.entry_date) / $3)::int AS _grp
,f.id, f.entry_date, f.pop, f.hip, f.lop, f.pcl, f.vop, f.poi
FROM foobar f
WHERE f.entry_date BETWEEN _start_date AND _end_date AND id = ANY(_ids)
ORDER BY f.id, f.entry_date, _grp;
ELSE
RETURN QUERY
SELECT ceil(rank() OVER (ORDER BY f.id, f.entry_date) / $3)::int -- no alias
,f.id, f.entry_date, f.pop, f.hip, f.lop, f.pcl, f.vop, f.poi
FROM foobar f
WHERE f.entry_date BETWEEN _start_date AND _end_date
ORDER BY f.id, f.entry_date, 1; -- ordinal pos. instead of col alias
END IF;
END;
$BODY$ LANGUAGE plpgsql;
The reason why I prefix IN parameters with _ is the same: avoid such naming conflicts.
You don't even have to use an alias for the computed column at all in this case. You could use the ordinal position in ORDER BY like I demonstrate in the second query. I quote the manual here:
Each expression can be the name or ordinal number of an output column
(SELECT list item), or it can be an arbitrary expression formed from
input-column values.

Related

How to find maximum of each column using Postgres Sql Functions

So I want MAX and MIN value from each column from table when execute function and input of that function is table.
for example-: Select * from public.f_count_MAX('table_name')
Any suggestions thanks in advance.
I have already created a function for finding null value which is below
CREATE OR REPLACE FUNCTION public.f_count_nulls(
_tbl regclass)
RETURNS TABLE(column_name text, missing_values bigint)
LANGUAGE 'plpgsql'
COST 100
STABLE PARALLEL SAFE
ROWS 1000
AS $BODY$
BEGIN
RETURN QUERY EXECUTE (
SELECT format(
$$
SELECT x.*
FROM (SELECT count(*) AS ct, %s FROM %s) t
CROSS JOIN LATERAL (VALUES %s) x(col, nulls)
ORDER BY nulls DESC, col DESC
$$, string_agg(format('count(%1$I) AS %1$I', attname), ', ')
, $1
, string_agg(format('(%1$L, ct - %1$I)', attname), ', ')
)
FROM pg_catalog.pg_attribute
WHERE attrelid = $1
AND attnum > 0
AND NOT attisdropped
-- more filters?
);
END
$BODY$;

How to do forward fill as a PL/PGSQL function

I am trying to create a pl/pgsql equivalent to the pandas 'ffill' function. The function should forward fill null values. In the example I can do a forward fill but I get errors when I try to create a function from my procedure. The function seems to reflect exactly the procedure but I get a syntax error at the portion ... as $1.
Why? What should I be reading to clarify?
-- Forward fill experiment
DROP TABLE IF EXISTS example;
create temporary table example(id int, str text, val integer);
insert into example values
(1, 'a', null),
(1, null, 1),
(2, 'b', 2),
(2,null ,null );
select * from example
select (case
when str is null
then lag(str,1) over (order by id)
else str
end) as str,
(case
when val is null
then lag(val,1) over (order by id)
else val
end) as val
from example
-- Forward fill function
create or replace function ffill(text, text, text) -- takes column to fill, the table, and the ordering column
returns text as $$
begin
select (case
when $1 is null
then lag($1 ,1) over (order by $3)
else $1
end) as $1
from $2;
end;
$$ LANGUAGE plpgsql;
Update 1: I did some additional experimenting taking a different approach. The code is below. It uses the same example table as above.
CREATE OR REPLACE FUNCTION GapFillInternal(
s anyelement,
v anyelement) RETURNS anyelement AS
$$
declare
temp alias for $0 ;
begin
RAISE NOTICE 's= %, v= %', s, v;
if v is null and s notnull then
temp := s;
elsif s is null and v notnull then
temp := v;
elsif s notnull and v notnull then
temp := v;
else
temp := null;
end if;
RAISE NOTICE 'temp= %', temp;
return temp;
END;
$$ LANGUAGE PLPGSQL;
CREATE AGGREGATE GapFill(anyelement) (
SFUNC=GapFillInternal,
STYPE=anyelement
);
select id, str, val, GapFill(val) OVER (ORDER by id) as valx
from example;
The resulting table is this:
I don't understand where the '1' in the first row of valx column comes from. From the raise notice output it should be Null and that seems a correct expectation from the CREATE AGGREGATE docs.
Correct call
Seems like your displayed query is incorrect, and the test case is just too reduced to show it.
Assuming you want to "forward fill" partitioned by id, you'll have to say so:
SELECT row_num, id
, str, gap_fill(str) OVER w AS strx
, val, gap_fill(val) OVER w AS valx
FROM example
WINDOW w AS (PARTITION BY id ORDER BY row_num); -- !
The WINDOW clause is just a syntactical convenience to avoid spelling out the same window frame repeatedly. The important part is the added PARTITION clause.
Simpler function
Much simpler, actually:
CREATE OR REPLACE FUNCTION gap_fill_internal(s anyelement, v anyelement)
RETURNS anyelement
LANGUAGE plpgsql AS
$func$
BEGIN
RETURN COALESCE(v, s); -- that's all!
END
$func$;
CREATE AGGREGATE gap_fill(anyelement) (
SFUNC = gap_fill_internal,
STYPE = anyelement
);
Slightly faster in a quick test.
Standard SQL
Without custom function:
SELECT row_num, id
, str, first_value(str) OVER (PARTITION BY id, ct_str ORDER BY row_num) AS strx
, val, first_value(val) OVER (PARTITION BY id, ct_val ORDER BY row_num) AS valx
FROM (
SELECT *, count(str) OVER w AS ct_str, count(val) OVER w AS ct_val
FROM example
WINDOW w AS (PARTITION BY id ORDER BY row_num)
) sub;
The query becomes more complex with a subquery. Performance is similar. Slightly slower in a quick test.
More explanation in these related answers:
Carry over long sequence of missing values with Postgres
Retrieve last known value for each column of a row
SQL group table by "leading rows" without pl/sql
db<>fiddle here - showing all with extended test case

why does this function not return the excpected tsrange[]

I have these table with this dataset
CREATE TABLE employee (entity_id uuid, valid tsrange);
INSERT into employee (entity_id, valid)
VALUES
('0006f79d-5af7-4b29-a200-6aef3bb0105f', tsrange('2009-05-23 02:00:00','2010-08-27 02:00:00')),
('0006f79d-5af7-4b29-a200-6aef3bb0105f', tsrange('2010-08-27 02:00:00','2010-10-27 02:00:00')),
('0006f79d-5af7-4b29-a200-6aef3bb0105f', tsrange('2011-05-23 02:00:00','infinity'))
for which I want to select an each continous aggregated tsrange, as an tsrange[], as for a specific entity_id.
For this I have this function
CREATE OR REPLACE FUNCTION tsrange_cluster_aggregate(_tbl regclass, selected_entity_id uuid)
RETURNS SETOF TSRANGE AS
$$
BEGIN
EXECUTE
FORMAT(
'SELECT tsrange(min(COALESCE(lower(valid), ''-infinity'')), max(COALESCE(upper(valid), ''infinity'')))
FROM (
SELECT *, count(nextstart > enddate OR NULL) OVER (ORDER BY valid DESC NULLS LAST) AS grp
FROM (
SELECT valid
, max(COALESCE(upper(valid), ''infinity'')) OVER (ORDER BY valid) AS enddate
, lead(lower(valid)) OVER (ORDER BY valid) As nextstart
FROM %1$I
where entity_id = %2$L
) a
) b
GROUP BY grp
ORDER BY 1;'
, _tbl , selected_entity_id)
USING _tbl, selected_entity_id;
END
$$ LANGUAGE plpgsql;
SELECT tsrange_cluster_aggregate('employee'::regclass, '0006f79d-5af7-4b29-a200-6aef3bb0105f');
Problem here is that output of this function is always empty?
I get the expected output when I run it outside an function?
https://dbfiddle.uk/?rdbms=postgres_12&fiddle=a09f8033091ea56e533880feebd0d5d4
You are missing a RETURN in your function:
CREATE OR REPLACE FUNCTION tsrange_cluster_aggregate(_tbl regclass, selected_entity_id uuid)
RETURNS SETOF tsrange[] AS
$$
BEGIN
RETURN QUERY EXECUTE
FORMAT(
' SELECT array_agg(j) FROM (
SELECT tsrange(min(COALESCE(lower(valid), ''-infinity'')), max(COALESCE(upper(valid), ''infinity'')))
FROM (
SELECT *, count(nextstart > enddate OR NULL) OVER (ORDER BY valid DESC NULLS LAST) AS grp
FROM (
SELECT valid
, max(COALESCE(upper(valid), ''infinity'')) OVER (ORDER BY valid) AS enddate
, lead(lower(valid)) OVER (ORDER BY valid) As nextstart
FROM %1$I
where entity_id = %2$L
) a
) b
GROUP BY grp
ORDER BY 1) z (j);'
, _tbl , selected_entity_id)
USING _tbl, selected_entity_id;
END
$$ LANGUAGE plpgsql;
Demo: db<>fiddle

Parse function param to inner query is not possible?

I am trying to execute this query but for some reason I seem to get this error
ERROR: syntax error at or near "$1"
LINE 8: FROM $1
Which I am not sure makes sense?
The function which should execute it is this
CREATE OR REPLACE FUNCTION tsrange_aggregate(_tbl regclass, selected_entity_id uuid, foreign_tsrange tsrange, OUT result Boolean)
RETURNS BOOLEAN AS
$$
BEGIN
EXECUTE
'SELECT $3 <# (SELECT tsrange(min(COALESCE(lower(valid), ''-infinity'')), max(COALESCE(upper(valid), ''infinity'')))
FROM (
SELECT *, count(nextstart > enddate OR NULL) OVER (ORDER BY valid DESC NULLS LAST) AS grp
FROM (
SELECT valid
, max(COALESCE(upper(valid), ''infinity'')) OVER (ORDER BY valid) AS enddate
, lead(lower(valid)) OVER (ORDER BY valid) As nextstart
FROM $1
where entity_id = $2
) a
) b
GROUP BY grp
ORDER BY 1);'
INTO result
USING _tbl, selected_entity_id, foreign_tsrange ;
END
$$ LANGUAGE plpgsql;
SELECT tsrange_aggregate('parent_registration'::regclass, '0006f79d-5af7-4b29-a200-6aef3bb0105f', tsrange('2011-05-23 02:00:00', '2013-05-23 02:00:00' ));
is it not possible to function parameter to inner query? or am I missing something here?
The error message suggests that postgres cannot cope with a table called $1. You have to concatenate the table name in the variable with the string, e.g. using FORMAT:
CREATE OR REPLACE FUNCTION myfunc(text)
RETURNS BOOLEAN AS
$$
DECLARE res boolean;
BEGIN
EXECUTE FORMAT('SELECT b FROM %I',$1) INTO res;
RETURN res;
END
$$ LANGUAGE plpgsql;
%I treats the argument value as an SQL identifier.
Demo: db<>fiddle

Order by in subquery (for jQuery jTable) doesn't work?

Some background:
My framework jQuery jTable, allows me to do pagination and sort columns, in my select query I need to retrieve n rows (from nth, to nth) and previously order the data by the selected column.
I have a table with n columns where would not exist some rows (this is an example):
To achieve the first requirement I wrote the follow procedure:
create or replace
PROCEDURE PR_SHOWVALUESOLD
(
PRMROWMIN IN NUMBER
, PRMROWMAX IN NUMBER
, CURSORRESULT OUT SYS_REFCURSOR
) AS
BEGIN
open CURSORRESULT for
select * from
(select v.*, rownum r,
(
select count(*) TOTALITEMS from TABLE1 v
) TOTALITEMS
from TABLE1 v
) d
where d.r >= PRMROWMIN and d.r <= PRMROWMAX;
END PR_SHOWVALUESOLD;
This work successfully, I execute the procedure with the follows parameters (PRMROWMIN = 6, PRMROWMAX = 9), the result of the procedure are in Output Varibles window.
Now comes the next step, I need to order the data before take from n to x row.
I rewrite the procedure to do this, but doesn't work:
CREATE OR REPLACE PROCEDURE PR_SHOWVALUES
(
PRMROWMIN IN NUMBER
, PRMROWMAX IN NUMBER
, PRMORDERCOL IN VARCHAR2
, PRMORDERDIR IN VARCHAR2
, CURSORRESULT OUT SYS_REFCURSOR
) AS
BEGIN
open CURSORRESULT for
select * from
(select v.*, rownum r,
(
select count(*) TOTALITEMS from TABLE1 v
) TOTALITEMS
from TABLE1 v
order by 'LOWER(' || PRMORDERCOL || ')' || ' ' || PRMORDERDIR
) d
where d.r >= PRMROWMIN and d.r <= PRMROWMAX;
END PR_SHOWVALUES;
I executed the modified procedure with the follows parameters:
PRMROWMIN := 6;
PRMROWMAX := 9;
PRMORDERCOL := 'COLUMNA';
PRMORDERDIR := 'DESC';
I expected the highlighted rows Query Result 2 window (but this new procedure retrieve the same data as old but disordered Output Variables Window):
How to achieve my requirements?
Thanks in advance.
This is your order by:
order by 'LOWER(' || PRMORDERCOL || ')' || ' ' || PRMORDERDIR
It is not applying the function lower(). Instead, it is concatenating the strings. You may mean:
order by LOWER(PRMORDERCOL) ' ' || PRMORDERDIR
I found a solution to my requirement.
I need to use DECODE to match every column to sort.
I can order in subquery, in this case I do two order by in two subqueries.
The documentation of PL/SQL DECODE function are in:
PL/SQL DECODE FUNCTION
The final Procedure are:
CREATE OR REPLACE PROCEDURE PR_SHOWVALUES
(
PRMROWMIN IN NUMBER
, PRMROWMAX IN NUMBER
, PRMORDERCOL IN VARCHAR2
, PRMORDERDIR IN VARCHAR2
, CURSORRESULT OUT SYS_REFCURSOR
) AS
BEGIN
open CURSORRESULT for
select * from (
select rownum r, v.* from
(
select * from
(
select * from table1 tbl
order by decode
(
UPPER(PRMORDERCOL),
'COLUMNA', LOWER(tbl.COLUMNA),
'COLUMNB', LOWER(tbl.COLUMNB),
LOWER(tbl.TABLE1_ID)
)
)
ORDER BY
CASE
WHEN UPPER(PRMORDERDIR) = 'DESC' THEN
ROWNUM * -1
ELSE
ROWNUM
END
) v
)
where r >= PRMROWMIN and r <= PRMROWMAX;
END PR_SHOWVALUES;
Acknowledgment to Jack David Baucum where I found the solution.