Incorporating a LOOP into a SQL - postgresql

I am fairly new at SQL and have not incorporated a Loop into a SQL statement previously. This SQL query from elan.elig returns data as shown in the grid below.
select (extract(year from age(case when terminationdate is null then
CURRENT_DATE else terminationdate END ,effectivedate ))) *12 +
(extract(month from age(case when terminationdate is null then
CURRENT_DATE else terminationdate END ,effectivedate )) +1)
as "mbrmonths" ,effectivedate
from elan.elig
Mbr Months Effective Date
1 10/1/2018
10 11/1/2018
2 11/1/2018
8 11/1/2018
8 11/1/2018
8 11/1/2018
2 11/1/2018
2 11/1/2018
7 11/1/2018
For each row from the query I need to execute the subsequent LOOP that spreads the memberMonth counts into the Year/Month buckets. The following Do LOOP does exactly this. I have been trying for some time now to determine how to incorporate the Loop into the SQL statement so that for each row read, it will pass the two variables and execute the Loop and then read the next row and continue on..
DO $$
declare
nbr_mem_months integer=5;
effectivedate date ='20190401';
ym char(6) =to_char(effectivedate,'YYYYMM');
begin
for r in 1..nbr_mem_months loop
update elan.pmpm set mbrmonths=mbrmonths+1 where yyyyymm=ym;
effectivedate=effectivedate + interval '1 month';
ym=to_char(effectivedate,'YYYYMM');
end loop;
end;
$$;
PMPM Buckets
yyyymm mbrmonths
201901 0
201902 0
201903 0
201904 1
201905 1
201906 1
201907 1
201908 1
201909 0
201910 0
201911 0
CREATE FUNCTION "UpdatePMPM"() RETURNS boolean
LANGUAGE plpgsql
AS
$$
DECLARE
nbr_mem_months NUMERIC;
effectivedate date;
ym char(6);
BEGIN
LOOP
ym=to_char(effectivedate,'YYYYMM');
nbr_mem_months=5;
UPDATE elan.pmpm set mbrmonths=mbrmonths+1 where yyyyymm=ym;
effectivedate=effectivedate + interval '1 month';
END LOOP;
RETURN TRUE;
END
$$;
*Response from the Select statement:
ERROR: function public.UpdatePMPM(integer, date, text) does not exist
Select public."UpdatePMPM"(5,cast('20190101' as date),cast('...
^
HINT: No function matches the given name and argument types.

The issue is calling the function with arguments but not specifying any when creating the function. So you need something like(not tested):
CREATE FUNCTION "UpdatePMPM"(nbr_mem_months integer, effectivdate date, some_arg varchar) RETURNS void
LANGUAGE plpgsql
AS
$$
DECLARE
ym varchar := to_char(effectivedate,'YYYYMM');
BEGIN
FOR r IN 1..nbr_mem_months LOOP
UPDATE elan.pmpm set mbrmonths = mbrmonths+1 where yyyyymm = ym;
effectivedate = effectivedate + interval '1 month';
ym=to_char(effectivedate,'YYYYMM');
END LOOP;
RETURN;
END
$$;
From the error it is not clear what the third argument is supposed to be, so that will clarification from you.

Related

make_date function does not exist in plpgsql

I've got a plpgsql function. I need to take the date 5 days from today, and then divide month into "fives" to takte the start of "last five". The problem is thay make_date does not exist in the posgres version that is used on the server....
create or replace function getFirstDayOfFive()
returns timestamp with time zone as $$
declare
firstDay timestamp;
startOp timestamp;
begin
startOp = now() - interval '5 day';
SELECT
make_date(
date_part('year', startOp)::int,
date_part('month', startOp)::int,
greatest(
floor(date_part('day', startOp) / 5) * 5,
1
)::int
)
INTO firstDay;
RETURN firstDay;
end;
$$
language plpgsql;
It worked fine last week, but now I got an error when I call it
ERROR: BŁĄD: function make_date(integer, integer, integer) does not exist
LINE 2: make_date(
^
HINT: There is no function matching provided name and arguments. Maybe you should cast data.
QUERY: SELECT
make_date(
date_part('year', startOp)::int,
date_part('month', startOp)::int,
greatest(
floor(date_part('day', startOp) / 5) * 5,
1
)::int
)
CONTEXT: PL/pgSQL function "getfirstdayoffive" line 7 at wyrażenie SQL
SQL state: 42883
What happened that earlier it worked and now it gives error?
[Edit]
I found out that make_date is available from postgresQL 9.4, but on the server there is posthresQL 9.1 is there any way to do the same in this old version od DB? I'm trying to replace the make_date with something like
create or replace function getFirstDayOfFive()
returns timestamp with time zone as $$
declare
firstDay timestamp;
startOp timestamp;
begin
startOp = now() - interval '5 day';
SELECT
date to_char(startOp, 'YYYY-MM-')||to_char(greatest(
floor(date_part('day', startOp) / 5) * 5,
1
)::int)
INTO firstDay;
RETURN firstDay;
end;
$$
language plpgsql;
I think you can simplify this by simply adding the desired number of days to the start of the month. Apparently you only want a date so I would also recommend to change the return type to date
create or replace function getfirstdayoffive()
returns date
as
$$
select date_trunc('month', current_date - 5)::date
+ (greatest(floor(extract(day from current_date - 5) / 5) * 5, 1))::int - 1;
$$
language sql
stable;

check leap year or not in PostgreSQL

How to check the given year is leap year or not in PostgreSQL.?
I tried below query.But it is showing error message.
select extract(year from hiredate)% 4 = 0 from emp
You must cast the extracted year to an integer.
select extract(year from hiredate)::integer % 4 = 0 from emp
However, that is the incorrect formula for a leap year. It is Every four years except every 100 except every 400. 1900 was not a leap year, but 2000 was.
create function is_leap_year(timestamp)
returns boolean as $$
declare y integer;
begin
y := extract(year from $1);
return (y % 4 = 0) and (y % 100 <> 0 or y % 400 = 0);
end
$$ language plpgsql;
Alternatively, you can check to see what day comes before March 1st in that year. This is safer as it will use Postgresql's internal logic.
create function is_leap_year(timestamp)
returns boolean as $$
begin
return date_part(
'day',
make_date(date_part('year', $1)::int, 3, 1) - '1 day'::interval
) = 29;
end
$$ language plpgsql;

Create multiple records automatically in postgres?

Is there a way for postgres to automatically generate child records with set parameters? I'm basically trying to create an employee timesheet and each time a new timesheet for a given date is created, I'd like to create a 7 child records (one record for each day of that given week for the user to fill in)
Something like this:
date (automatically generated on a weekly basis) | hours | timesheet_id(FK) | project_id(FK)
2019-01-01 8 1 2
2019-01-02 10 1 2
2019-01-03 8 1 2
2019-01-04 8 1 2
2019-01-05 0 1 2
2019-01-06 0 1 2
2019-01-07 9 1 2
#Z4-tier is correct but the trigger function does not need a loop as it can be reduced to a single insert statement.
create or replace function create_timesheet_days()
returns trigger
language 'plpgsql'
as $$
begin
insert into timesheet_days(timesheet_id, week_day)
select new.timesheet_id, wk_day
from generate_series (new.timesheet_date, new.timesheet_date+interval '6 day', interval '1 day') wk_day;
return new;
end;
$$ ;
as mentioned in the comments, this is a textbook case for using triggers.
You'll need a procedure that handles creating the new records. This might work:
create or replace function create_timesheet_days()
returns trigger as $BODY$
declare counter INTEGER := 0;
begin
while counter < 6 loop
insert into my_schema.timesheet_week
(timesheet_date, hrs, timesheet_id, project_id)
select
NEW.timesheet_date + counter as timesheet_date,
0 as hrs,
NEW.timesheet_id as timesheet_id,
project.project_id as project_id
from my_schema.project;
counter := counter + 1;
end loop;
return NEW;
END;
$BODY$
language 'plpgsql';
Then you could create a trigger like this:
CREATE TRIGGER create_timsheet_days_trigger
BEFORE INSERT
ON my_schema.timesheet_week
FOR EACH ROW
EXECUTE PROCEDURE
create_timesheet_days();

how to get the next date for a day of month

thanks for reading, this is the situation
I have a current_date and a day of month, so i need to know what will be the next date for this day of month, having in mind that some month don't have 30 and 31.
Example:
current_date = '2018-09-24'
day_of_week = 31
Expected result: '2018-12-31'
Currently i have this:
create or replace function next_diff(vals int[], current_val int) returns int as
$$
declare v int;
declare o int := vals[1];
begin
foreach v in array vals loop
if current_val >= o and current_val < v then
return v - current_val;
end if;
o := v;
end loop;
return vals[1] - current_val;
end;
$$ language plpgsql;
and this:
create or replace function next_day_of_month(days_of_month int[], curr_date date) returns date as
$$
declare cur_dom int := extract(day from curr_date);
declare next_diff int := next_diff(days_of_month, cur_dom);
begin
if next_diff < 0 then
curr_date := curr_date + '1 months'::interval;
end if;
curr_date := curr_date + (next_diff || 'days')::interval;
return curr_date;
end;
$$ language plpgsql;
but for this calling:
select next_day_of_month(array[31], '2018-09-24');
i am getting:
"2018-10-01"
Extra example
If i have this value
current_date = '2018-02-01'
day_of_week = 31
i will need the next month with 31th but i can't get '2018-02-31' because February don't have 31th then i should get '2018-02-31' because March have 31th.
Conclusion
if the month don't have the specified day must ignore the month and jump to the next.
thanks for all
Final method
Using Carlos Gomez answer, i create this PostgreSQL function and work perfectly:
create or replace function next_day_date(curr_date date, day_of_month int) returns date as
$$
declare next_day date;
begin
SELECT next_day_date into next_day FROM (
SELECT make_date_nullable(EXTRACT(year from n.month)::int, EXTRACT(month from n.month)::int, day_of_month) AS next_day_date
FROM (
SELECT generate_series(curr_date, curr_date + '3 months'::interval, '1 month'::interval) as month
) n
) results
WHERE results.next_day_date IS NOT NULL and results.next_day_date > curr_date LIMIT 1;
return next_day;
end;
$$ language plpgsql;
just add other filter in where clause and results.next_day_date > curr_date to prevent get the same or previous values for specified date
Thanks everyone for helping
Thenks Carlos you are the best
Gracias carlos eres el mejor :)
Your examples don't really match up but I think I know what you are trying to solve for (your first example result should be '2018-10-31' since October has 31 days and your second example result should be '2018-03-31'). It seems that given a date and a day of month you want to find the next month that has that day of month. To do this, I would do the following:
This function just wraps make_date to let it return null since it throws an exception if a date given to it is out of bounds (like February 30).
CREATE OR REPLACE FUNCTION make_date_nullable(year int, month int, day int)
RETURNS date as $$
BEGIN
RETURN make_date(year, month, day);
EXCEPTION WHEN others THEN RETURN null;
END;
$$ language plpgsql;
This SELECT first generates the next three months starting with the current one, then makes date out of them with your provided day_of_month and finally gets the first one that isn't null (exists according to postgresql.
SELECT next_day_date FROM (
SELECT make_date_nullable(EXTRACT(year from n.month)::int, EXTRACT(month from n.month)::int, day_of_month) AS next_day_date
FROM (
SELECT generate_series(current_date, current_date + '3 months'::interval, '1 month'::interval) as month
) n
) results
WHERE results.next_day_date IS NOT NULL LIMIT 1;
Hope this helps!

PostgreSQL checking a previous record's element

I need to check the previous record's element to make sure the date I query doesn't fall within a specific range between ending date and 7 days before starting date. I have the following code:
create or replace function eight (date) returns text as $$
declare
r record;
checkDate alias for $1;
begin
for r in
select * from periods
order by startDate
loop
if (checkDate between r.startDate and r.endDate) then
return q3(r.id);
elsif (checkDate between (r.startDate - interval '7 days') and r.startDate) then
return q3(r.id);
elsif (checkDate between (lag(r.endDate) over (order by r.startDate)) and (r.startDate - interval '8 days')) then
return q3(r.id);
end if;
end loop;
return null;
end;
$$ language plpgsql;
So basically, I need to check for the following:
If the query date is between the starting and ending dates
If the query date is 7 days before the start of the starting date
If the query date is between ending date and the starting date
and return the id that is associated with that date.
My function seems to work fine in most cases, but there are cases that seem to give me 0 results (when there should always be 1 result) is there something missing in my function? I'm iffy about the last if statement. That is, trying to check from previous records ending date to current records starting date (with the 7 day gap)
EDIT: no dates overlap.
Edit: Removed the part about RETURN NEXT - I had misread the question there.
Doesn't work the way you have it. A window function cannot be called like that. Your record variable r is like a built-in cursor in a FOR loop. Only the current row of the result is visible inside the loop. You would have to integrate the window function lag() it into the initial SELECT.
But since you are looping through the rows in a matching order anyway, you can do it another way.
Consider this largely rewritten example. Returns at the first violating row:
CREATE OR REPLACE FUNCTION q8(_day date)
RETURNS text AS
$BODY$
DECLARE
r record;
last_enddate date;
BEGIN
FOR r IN
SELECT *
-- ,lag(r.endDate) OVER (ORDER BY startDate) AS last_enddate
-- commented, because I supply an alternative solution
FROM periods
ORDER BY startDate
LOOP
IF _day BETWEEN r.startDate AND r.endDate THEN
RETURN 'Violates condition 1'; -- I return differing results
ELSIF _day BETWEEN (r.startDate - 7) AND r.startDate THEN
RETURN 'Violates condition 2';
ELSIF _day BETWEEN last_enddate AND (r.startDate) THEN
-- removed "- 7 ", that is covered above
RETURN 'Violates condition 3';
END IF;
last_enddate := r.enddate; -- remember for next iteration
END LOOP;
RETURN NULL;
END;
$BODY$ LANGUAGE plpgsql;
More hints
Why the alias for $1? You named it _day in the declaration already. Stick to it.
Be sure to know how PostgreSQL handles case in identifiers. ( I only use lower case.)
You can just add / subtract integers (for days) from a date.
Are you sure that lag() will return you something? I'm pretty sure that this is out of context here. Given that rows from periods are selected in order, you can store the current startDate in a variable, and use it in the if statement of the next cycle.
SET search_path='tmp';
DROP table period;
CREATE table period
( start_date DATE NOT NULL
, end_date DATE
);
INSERT INTO period(start_date ,end_date) VALUES
( '2012-01-01' , '2012-02-01' )
, ( '2012-02-01' , '2012-02-07' )
, ( '2012-03-01' , '2012-03-15' )
, ( '2012-04-01' , NULL )
, ( '2012-04-17' , '2012-04-21' )
;
DROP FUNCTION valid_date(DATE) ;
CREATE FUNCTION valid_date(DATE) RETURNS boolean
AS $body$
declare
found boolean ;
zdate ALIAS FOR $1;
begin
found = false;
SELECT true INTO found
WHERE EXISTS (
SELECT * FROM period p
WHERE (p.start_date > zdate
AND p.start_date < zdate + interval '7 day' )
OR ( p.start_date < zdate AND p.end_date > zdate )
OR ( p.start_date < zdate AND p.end_date IS NULL
AND p.start_date >= zdate - interval '7 day' )
)
;
if (found = true) then
return false;
else
return true;
end if;
end;
$body$ LANGUAGE plpgsql;
\echo 2011-01-01:true
SELECT valid_date('2011-01-01' );
\echo 2012-04-08:false
SELECT valid_date('2012-04-08' );
\echo 2012-04-30:true
SELECT valid_date('2012-04-30' );
BTW: I really think that the required functionality should be implemented as a table constraint, imposed by a trigger function (that might be based on the above function).