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
Related
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
I have a command to run in pgadmin which looks like below:
SELECT format('SELECT * FROM %I.%I CROSS JOIN LATERAL json_to_record(%I::json) AS rs(%s)', 'public', 'vehicles', 'column_A', array_to_string(
(SELECT ARRAY(SELECT DISTINCT col FROM vehicles CROSS JOIN LATERAL json_object_keys(column_A::json) AS t(col) ORDER BY col)), ' text , '
) || ' text')
It prints a string starting with SELECT statement.
How do I get the result from the query straight from the string returned by the FORMAT?
I have tried something like:
DO
$$
WITH str as( SELECT format('SELECT * FROM %I.%I CROSS JOIN LATERAL json_to_record(%I::json) AS rs(%s)', 'public', 'vehicles', 'column_A', array_to_string(
(SELECT ARRAY(SELECT DISTINCT col FROM vehicles CROSS JOIN LATERAL json_object_keys(column_A::json) AS t(col) ORDER BY col)), ' text , '
) || ' text'))
BEGIN EXECUTE str;
END
$$
However, I got an error message saying:
ERROR: syntax error at or near "WITH"
What have I missed here? Please advise!!
Updated answer
After combining answers from the experts below, here is the updated version for future reference:
do $$
DECLARE
query text;
begin
query := format('SELECT * FROM %I.%I CROSS JOIN LATERAL json_to_record(%I::json) AS rs(%s)', 'public', 'vehicles', 'column_A', array_to_string(
(SELECT ARRAY(SELECT DISTINCT col FROM vehicles CROSS JOIN LATERAL json_object_keys(column_A::json) AS t(col) ORDER BY col)), ' text , '
) || ' text');
execute format('create or replace temp view tmp_view_vehicles as %s', query);
end $$;
select * from tmp_view_vehicles;
Thank you everyone & your patience!
If you don't want to create the stored function but want to get the result using anonymous do block then you could to use temporary view:
do $$
begin
execute format('create or replace temp view tmp_view_123 as select ...', ...);
end $$;
select * from tmp_view_123;
Created view is visible for the current session only.
demo
You are mixing up SQL and PL/pgSQL syntax, and not in a very consistent fashion.
Define a PL/pgSQL variable:
DO
$$DECLARE
query text;
result record;
BEGIN
query := format(...);
EXECUTE query INTO result;
END;$$;
The plain answer is
do language plpgsql
$$
begin
EXECUTE format('SELECT ....' <your code here>);
end;
$$;
However anonymous blocks do not return anything. Maybe you'll have to shape the block as a table-returning function.
Edit
I do not think that there is a straightforward way to do this - change the return table structure of a function dynamically. But you can return a single json column with key-value pairs inside.
Here is such a function:
create or replace function query_to_jsonset(qr text) returns setof json as
$$
begin
return query execute 'SELECT row_to_json(dyntbl) FROM ('||qr||') AS dyntbl';
end;
$$ language plpgsql;
and then your query will look simple as that:
select js from query_to_jsonset(format(....)) js;
Please note that query_to_jsonset is unsafe.
In the below query I am getting the values in a array format and I want to get column name too.
CREATE OR REPLACE FUNCTION load_page(IN _session integer)
RETURNS TABLE(col1 text, col2 text) AS
$BODY$
BEGIN
RETURN QUERY
select
(SELECT array_agg(sq.*)
FROM (SELECT user_id, user_name
FROM "user"
) sq
)::text,
(SELECT array_agg(sq.*)
FROM (SELECT client_id, client_name ,client_desc
FROM "clients"
) sq
)::text;
END;
$BODY$ LANGUAGE plpgsql STABLE;
The Result is :
"("{""(2,Test)"",""(5,Santhosh)"",""(3,Test1)""}","{""(1,Test1,Test1)"",""(2,test2,test2)"",""(3,test3,test3)""}")"
You could simply type them and concat into one column to use it as a row in outer select:
CREATE OR REPLACE FUNCTION load_page(IN _session integer)
RETURNS TABLE(col1 text, col2 text) AS
$BODY$
BEGIN
RETURN QUERY
select
(SELECT array_agg(sq.*)
FROM (SELECT concat('user_id: ',user_id,', user_name: ', user_name)
FROM "user"
) sq
)::text,
(SELECT array_agg(sq.*)
FROM (SELECT concat('client_id: ',client_id,', client_name: ',client_name,', client_desc: ',client_desc)
FROM "clients"
) sq
)::text;
END;
$BODY$ LANGUAGE plpgsql STABLE;
If you want to discard information about particular column if it's value is NULL you can append column names conditionally (with a CASE statement).
The below function does not produce the curly braces or the flood of double quotes, but it does include the column names and it is quite a bit more concise and readable and efficient than your initial version.
The _session parameter is never used, but I shall assume that this is all part of some larger query/function.
CREATE OR REPLACE FUNCTION load_page(IN _session integer, OUT col1 text, OUT col2 text)
RETURNS SETOF record AS $BODY$
BEGIN
SELECT string_agg('user_id: ' || user_id ||
', user_name: ' || user_name, ',')
INTO col1
FROM "user";
SELECT string_agg('client_id: ' || client_id ||
', client_name: ' || client_name ||
', client_desc: ' || client_desc, ',')
INTO col2
FROM "clients";
RETURN NEXT;
END;
$BODY$ LANGUAGE plpgsql STABLE;
In the Below Postgresql Function i am trying to get results from 2 different tables but it throws error ERROR: 42601: a column definition list is required for functions returning "record".Can anyone please help me.
CREATE OR REPLACE FUNCTION load_page_record(IN _session INT) RETURNS RECORD AS
$$
DECLARE r1 RECORD;
DECLARE r2 RECORD;
DECLARE RESULT RECORD;
BEGIN
SELECT array_agg(sq.*) AS arr INTO r1
FROM (SELECT user_id, user_name
FROM "user"
) sq;
SELECT array_agg(sq.*) AS arr INTO r2
FROM (SELECT client_id, client_name
FROM "clients"
) sq;
SELECT r1.arr, r2.arr INTO RESULT;
RETURN RESULT;
END;
$$ LANGUAGE plpgsql;
It returns a record,
so you should call the function as below,
select load_page_record(5);
The error come if you call it as a table
select * from load_page_record(5);
If you want to return a table place you query with join inside the body as follows,
CREATE OR REPLACE FUNCTION load_page_record1(IN _session INT)
RETURNS TABLE (column1 integer, column2 integer) as
$BODY$
SELECT column1, column2
FROM
table1 a
join
table2 b
ON a.id = b.id
$BODY$
LANGUAGE plpgsql;
try this, procedur return table
CREATE OR REPLACE FUNCTION load_page_record(IN _session INT)
RETURNS table(col1 record[],col2 record[]) AS
$BODY$
BEGIN
RETURN QUERY
select
(SELECT array_agg(sq.*)
FROM (SELECT user_id, user_name
FROM "user"
) sq
),
(SELECT array_agg(sq.*)
FROM (SELECT client_id, client_name
FROM "clients"
) sq
);
END;
$BODY$ LANGUAGE plpgsql stable;
edit: convert to text, try it
CREATE OR REPLACE FUNCTION load_page_record(IN _session INT)
RETURNS table(col1 text,col2 text) AS
$BODY$
BEGIN
RETURN QUERY
select
(SELECT array_agg(sq.*)
FROM (SELECT user_id, user_name
FROM "user"
) sq
)::text,
(SELECT array_agg(sq.*)
FROM (SELECT client_id, client_name
FROM "clients"
) sq
)::text;
END;
$BODY$ LANGUAGE plpgsql stable;
try with text:
CREATE OR REPLACE FUNCTION load_page_record(IN _session INT) RETURNS text AS
$$
DECLARE r1 RECORD;
DECLARE r2 RECORD;
DECLARE RESULT text;
BEGIN
SELECT array_agg(sq.*) AS arr INTO r1
FROM (SELECT 'fdfdfd','fdfdd'
) sq;
SELECT array_agg(sq.*) AS arr INTO r2
FROM (SELECT 'dsds','sdsd'
) sq;
SELECT r1.arr, r2.arr INTO RESULT;
RETURN RESULT;
END;
$$ LANGUAGE plpgsql;
and then simply:
select * from load_page_record(8);
but I hope you are aware of the fact that this instruction SELECT r1.arr, r2.arr INTO RESULT; will only assign the first column to RESULT?
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.