Use a function to handle a negative offset - sql

I am creating a Postgresql function in order to handle a negative offset. When the offset is negative then my function should return 0. However, when I try to run this function, there is an error near "row", I am not sure why. What is wrong with my conditional branch?
CREATE OR REPLACE FUNCTION
calc_offset(row integer, padding integer) RETURNS integer AS $$
BEGIN
if (row-1-padding) < 0 then return 0;
else return (row-1-padding);
END;
$$ LANGUAGE plpgsql;
WITH all_the_trimmings AS (
SELECT ui.id, ui.name, ui.time,
row_number() over(order by name asc) as rownumber
FROM user_infos ui
), my_row AS (
SELECT * FROM all_the_trimmings WHERE id=1
), the_slice AS (
SELECT * FROM all_the_trimmings LIMIT 5
OFFSET calc_offset((SELECT rownumber FROM my_row)::int,2)
)
SELECT * from my_row
UNION ALL
SELECT * from the_slice;
SQL fiddle for the full picture

Your function calc_offset() can be replaced with this simple SQL function:
CREATE OR REPLACE FUNCTION calc_offset(p_row int, padding int)
RETURNS int AS 'SELECT GREATEST ($1 - $2 - 1, 0)' LANGUAGE sql;
And you don't need the function at all. Just use GREATEST with a subselect:
WITH all_the_trimmings AS (
SELECT ui.id, ui.name, ui.time, row_number() OVER (ORDER BY name) AS rn
FROM user_infos ui
)
, my_row AS (
SELECT * FROM all_the_trimmings WHERE id=1
)
SELECT * FROM my_row
UNION ALL
SELECT * FROM all_the_trimmings LIMIT 5
OFFSET GREATEST ((SELECT rn - 3 FROM my_row), 0);
Or simpler:
WITH cte AS (
SELECT ui.id, ui.name, ui.time, row_number() OVER (ORDER BY name) AS rn
FROM user_infos ui
)
SELECT * FROM cte WHERE id = 1
UNION ALL
SELECT * FROM cte LIMIT 5
OFFSET GREATEST ((SELECT rn - 3 FROM cte WHERE id = 1), 0);
SQL Fiddle showing both.

The row parameter is a reserved word. Try replacing it with another name. You were also missing an end if;
CREATE OR REPLACE FUNCTION
calc_offset(p_row integer, padding integer) RETURNS integer AS $$
BEGIN
if (p_row-1-padding) < 0 then
return 0;
else
return (p_row-1-padding);
end if;
END;
$$ LANGUAGE plpgsql;

Related

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

PL/SQL procedure to output line the given date if not existing, latest date should be given

I have this table informationvalues with the contents:
Now I create a procedure where I need to input a date parameter which should output line the correct attr with given price. If the date doesn't exist the latest date should be selected.
The solution table for to_date('01-jan-19') would look like this:
This would be then output line in the procedure.
Should I select to correct tuple and output line it or would it be best to just bulk collect everything and then check in a for loop with an if statement what tuple I need to display.
What I have so far:
A select statement with the tuples I am looking for:
create or replace procedure print_inf_value(closingDate Date) is
cursor d1 (closingDate Date) is
select t.attr, t.dateOfValue, t.price
from (
select i.*,
row_number() over (
partition by attr
order by case when dateOfValue = closingdate then 1 else 2 end, dateOfValue desc
) rn
from InformationValues i
) t
where t.rn = 1;
BEGIN
dbms_output.put_line('Information Value ');
dbms_output.put_line('--------------------------------');
FOR d1_rec IN d1 LOOP
dbms_output.put_line(d1_rec.attr || ' ' || d1_rec.price );
END LOOP;
END;
Or a procedure where I bulk collect everything and then I need to sort out what tuple I need:
create or replace procedure print_inf_value(closingDate Date) is
TYPE d1 IS TABLE OF informationvalues%rowtype;
emps d1;
begin select * bulk collect into emps
from informationvalues;
FOR i IN 1 .. emps.COUNT LOOP
if emps(i).dateofvalue = closingDate then
dbms_output.put_line(emps(i).attr || ' ' || emps(i).price );
/*else*/
end if;
END LOOP;
END;
Both are not working right, so what am I missing to display tuple with the correct date.
Please try:
CREATE OR REPLACE PROCEDURE print_inf_value (closingDate DATE)
IS
BEGIN
DBMS_OUTPUT.put_line (RPAD ('ATTR', 20) || RPAD ('PRICE', 20));
FOR o
IN (select attr, trim(case when price < 1 then to_char(price,90.9) else to_char(price) end) price from (
select attr, price, dateofvalue,
row_number() over (partition by attr order by dateofvalue desc) rn from informationvalues
) i where dateofvalue = closingdate
or (rn = 1 and not exists (select 1 from informationvalues iv where iv.attr = i.attr and dateofvalue = closingdate) )
)
LOOP
DBMS_OUTPUT.put_line (RPAD (o.attr, 20) || RPAD ( o.price, 20));
END LOOP;
END;
Sample execution:
set serveroutput on;
begin
print_inf_value(date'2019-01-01');
end;
Output:
ATTR PRICE
age 2
electronics 0.5
gender 3
hobbies 0.5
homeAddress 7
maritalStatus 1
mobilePhone 5
musicTaste 0.1
socialContacts 1

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.

postgresql query

I have a table with one column (t1date) as:
Table1
t1date
----------
2011-05-24
There is also a function which takes one parameter dfunction(fdate). I want to pass the value of the t1date to the function. Something like:
SELECT *
FROM dfunction(SELECT t1date
FROM Table1
ORDER BY t1date
LIMIT 1)
which is equivalent to
SELECT *
FROM dfunction('2011-05-24')
I don't want to write another function for this. What I'm looking for is to use the select statement. Is it possible? If so how.
select dfunction(t1date) from Table1 order by t1date asc limit 1;
I understand, a subquery, when used as a scalar expression, should be enclosed in parentheses. So, how about this:
SELECT *
FROM dfunction(
(SELECT t1date
FROM Table1
ORDER BY t1date
LIMIT 1)
)
I'm having difficulty understanding exactly what you want. I have a few options, perhaps you can explain more if neither of these are what you want?
create table foo1 ( t1 date, id int );
insert into foo1 values ('2001-01-01', 1), ('2002-01-01', 2), ('2003-01-01', 3), ('2004-01-01', 4);
create or replace function foo1(date) returns setof foo1 as $$ select * from foo1 where $1 > t1 order by t1 asc; $$ language 'sql';
select * from foo1('2002-02-02');
create or replace function foo2(text) RETURNS SETOF foo1 as $$
DECLARE
q alias for $1;
r foo1%rowtype;
BEGIN
for r in execute q loop
return next r;
end loop;
return;
END;
$$ language 'plpgsql';
select * from foo2('select * from foo1 order by t1 asc limit 1');
drop table foo1 cascade;