Calling a Function from SQL - postgresql

This is my Function :
CREATE FUNCTION "UpdatePMPM"(nbr_mem_months integer, effectivedate date) 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
$$;
and when I call it manually from pgAdmin client it works perfectly.
Select public."UpdatePMPM"(5, '2016-04-01')
However, I am getting an error when calling it from within a SQL query:
select cast((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 integer)
as "mbrmonths" ,effectivedate ,public."UpdatePMPM"(mbrmonths, effectivedate)
from elan.elig
order by 1
ERROR: column "mbrmonths" does not exist
LINE 5: ...s "mbrmonths" ,effectivedate ,public."UpdatePMPM"(mbrmonths,...
Any help would be appreciated.

You cannot use a column alias in the SELECT list to reference another column of the same list.
Either repeat the expression or use a subquery:
SELECT mbrmonths,
effectivedate,
public."UpdatePMPM"(mbrmonths, effectivedate)
FROM (select cast(
(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 integer) as "mbrmonths",
effectivedate,
from elan.elig) AS subq
order by 1;

The type of second argument is text, not date.
The Correct call is
Select public."UpdatePMPM"(5, '2016-04-01'::date)

Related

Postgres how to return multiple columns in subquery

I have a simple Postgres function:
CREATE OR REPLACE FUNCTION public.drivers_have_unsatisfied_documents()
RETURNS driver_expiring_documents
AS $function$
DECLARE
unsatified_documents driver_expiring_documents;
expiring_doc driver_expiring_documents%rowtype;
expiring_doc_driver_ids text[];
expired_doc_driver_ids text[];
expiring_date date;
driver_id_arg text;
BEGIN
unsatified_documents := array(
select expiration_date, driver_id from all_requirements_driver_documents where expiration_date <= (now() + interval '1 month')::DATE and expiration_date > (now() - interval '7 day')::DATE
union
select expiration_date, driver_id from all_requirements_vehicle_documents where expiration_date <= (now() + interval '1 month')::DATE and expiration_date > (now() - interval '7 day')::DATE
);
-- Do some more stuff, code intentionally removed ---
return unsatified_documents;
end;
$function$ stable language 'plpgsql';
This is throws error saying
multiple columns in subquery
Can someone please help me fix it?

Incorporating a LOOP into a SQL

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.

postgresql: use timestamp variable within pl pgsql

Im new using pl pgsql. I want to concatenate two variables but im gotting always same error: time_ variable is not known
Let's say that date_ is of type date and time_ is of type time. The error came from this row:
sum(extract(epoch from (least(s.end, gs.date_+time_) - greatest(s.beg, gs.date_))) / 60) as Timing
My code is below
delcare
time_ time;
Begin
execute $$SELECT CURRENT_TIMESTAMP::time FROM $$||result_table INTO time_;
execute $$SELECT MAX(date_) FROM $$||result_table INTO max_date;
IF max_date is not NULL THEN
execute $$DELETE FROM $$||result_table||$$ WHERE date_ >= $$||quote_literal(max_date);
ELSE
max_date := 'XXXXXXX';
end if;
execute $$
INSERT INTO $$result_table$$
(Id, gs.date_, TIME, timing)
SELECT * from (
select
Id, gs.date_,
(case
When TRIM(set) ~ '^OPT[0-9]{3}/MINUTE/$'
Then 'minute'
When TRIM(set) ~ '^OPT[0-9]{3}/SECOND/$'
Then 'second' as TIME,
sum(extract(epoch from (least(s.end, gs.date_+time_) -
greatest(s.beg, gs.date_)
)
) / 60) as Timing
from source s cross join lateral
generate_series(date_trunc('day', s.beg), date_trunc('day',
least(s.end,
CASE WHEN $$||quote_literal(max_date)||$$ = 'XXXXXXX'
THEN (current_date)
ELSE $$||quote_literal(max_date)||$$
END)
), interval '1 day') gs(date_)
where ( (beg, end) overlaps ($$||quote_literal(max_date)||$$'00:00:00', $$||quote_literal(max_date)||$$'23:59:59'))
group by id, gs.date_, TIME
) as X
where ($$||quote_literal(max_date)||$$ = X.date_ and $$||quote_literal(max_date)||$$ != 'XXXXXXX')
OR ($$||quote_literal(max_date)||$$ ='XXXXXXX')
Dynamic SQL should be generated through format() and parameters should not be passed as string literals, but through placeholders and using.
Your code is really hard to read, incomplete and there are some substantial syntax errors which stem from e.g. a missing END for the CASE and parentheses not properly paired. So the following code might still contain some errors as I apparently have no way of testing it.
But as your main SELECT does not seem to use dynamic SQL at all, all the quote_literal() and string concatenation is unnecessary, just use the variables directly.
As max_date is supposed to be a date value you can assign the string 'XXXXX' to it, but if you use the max_date directly, you can get rid of that check as far as I can tell.
declare
time_ time;
max_date date;
result_table text := 'contract_frequency';
table_schema text := 'public';
Begin
time_ := localtime;
execute format('SELECT MAX(date_) FROM %I.%I', table_schema, result_table) into INTO max_date;
IF max_date is not NULL THEN
execute format('DELETE FROM %I.%I WHERE date_ >= $1', table_schema, result_table) using max_date;
ELSE
-- you replace XXXX with current_date in the CASE expression
-- later on, so using current_date here seems the right thing to do
max_date := current_date;
end if;
SELECT *
from (
select
Id, gs.date_,
case
When TRIM(set) ~ '^OPT[0-9]{3}/MINUTE/$' Then 'minute'
When TRIM(set) ~ '^OPT[0-9]{3}/SECOND/$' Then 'second' as TIME,
end
sum(extract(epoch from (least(s.end, gs.date_+time_) - greatest(s.beg, gs.date_) ) ) / 60) as Timing
from source s
cross join lateral
generate_series(date_trunc('day', s.beg), date_trunc('day', least(s.end, max_date)), interval '1 day') gs(date_)
where (beg, end) overlaps (max_date::timestamp, max_date + time '23:59:59')
group by id, gs.date_, TIME
) as X
where (max_date = X.date_ and max_date <> current_date)
OR (max_date = current_date)
end;

Return table type from a function in PostgreSQL

I have a function with RETURNS TABLE, and I want to return certain columns from my source table. When I execute the function, it gives no error but also returns no rows although it should.
What's wrong with my function?
CREATE OR REPLACE FUNCTION ccdb.fn_email_details_auto()
RETURNS TABLE (code integer, area smallint, action smallint
, flag smallint, ucount integer, view_cnt integer) AS
$BODY$
DECLARE
sec_col refcursor;
cnt integer;
sec_code ccdb.update_qtable%ROWTYPE;
BEGIN
SELECT COUNT(DISTINCT section_code) INTO cnt
FROM ccdb.update_qtable
WHERE entry_time::date = now()::date - interval '1 day';
OPEN sec_col FOR
SELECT * FROM ccdb.update_qtable
WHERE entry_time::date = now()::date - interval '1 day';
FOR i IN 1..cnt
LOOP
FETCH sec_col INTO sec_code;
PERFORM section_code, ddu_area, ddu_action, status_flag
, ccdb_ucount, ccdb_view_cnt
FROM ccdb.update_qtable
WHERE entry_time::date = now()::date - interval '1 day'
AND section_code = sec_code.section_code
ORDER BY ddu_area, ddu_action;
END LOOP;
CLOSE sec_col;
END;
$BODY$
LANGUAGE plpgsql VOLATILE COST 100;
Your function is doing a lot of empty work.
You could replace the tedious and expensive explicit cursor with a FOR loop using a cursor implicitly. But don't bother, and radically simplify with a single query instead. Optionally wrapped into an SQL function:
CREATE OR REPLACE FUNCTION ccdb.fn_email_details_auto()
RETURNS TABLE (code integer, area smallint, action smallint, flag smallint
, ucount integer, view_cnt integer)
LANGUAGE sql AS
$func$
SELECT u.section_code, u.ddu_area, u.ddu_action, u.status_flag
, u.ccdb_ucount, u.ccdb_view_cnt
FROM ccdb.update_qtable u
WHERE u.entry_time >= now()::date - 1
AND u.entry_time < now()::date -- sargable!
ORDER BY u.section_code, u.ddu_area, u.ddu_action;
$func$;
Should be much faster while returning the same.
Also, use this:
WHERE u.entry_time >= now()::date - 1
AND u.entry_time < now()::date
instead of:
WHERE entry_time::date = now()::date - interval '1 day'
The alternative is "sargable" and can use a plain index on (entry_time), which is crucial for performance.
I was able to solve this issue by using a RETURN QUERY for the SELECT statement where I was using PERFORM.
The below mentioned query helped me achieve my requirement.
CREATE OR REPLACE FUNCTION ccdb.fn_email_details_auto()
RETURNS TABLE (code integer, area smallint, action smallint, flag smallint, ucount integer, view_cnt integer) AS
$BODY$
DECLARE
sec_col refcursor;
cnt integer;
sec_code ccdb.update_qtable%ROWTYPE;
BEGIN
SELECT COUNT(DISTINCT section_code)
INTO cnt
FROM ccdb.update_qtable
WHERE entry_time::date = now()::date - interval '1 day';
OPEN sec_col FOR
SELECT DISTINCT ON (section_code)* FROM ccdb.update_qtable WHERE entry_time::date = now()::date - interval '1 day';
FOR i IN 1..cnt
LOOP
FETCH sec_col INTO sec_code;
RETURN QUERY
SELECT section_code, ddu_area, ddu_action, status_flag, ccdb_ucount, ccdb_view_cnt
FROM ccdb.update_qtable
WHERE entry_time::date = now()::date - interval '1 day' AND section_code = sec_code.section_code
ORDER BY ddu_area, ddu_action;
END LOOP;
CLOSE sec_col;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;

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).