Create function or stored proc with interval variable - sql

I am new to PostgreSQL but have been working with MSSQL for years. I am currently working with PostgreSQL 13.
In MSSQL I have a script that creates an offset for dates so that I can create a fiscal calendar in the same date table as my regular calendar. I just add the offset to the date in the row and it adjusts my fiscal year as needed.
I am trying to preform something like this in PostgreSQL. I have some code working to do what I want. However, now I am trying to write a function or stored procedure to automate this code. I am having issues converting the code to work in a function or stored procedure.
My code I have working looks like this. This is a very trimmed down version of the code for illustration purposes.
DO $$
DECLARE daydiff INTERVAL;
DECLARE startdate DATE;
DECLARE enddate DATE;
DECLARE fiscalstartdate DATE;
BEGIN
-- Populate these variables
startdate:=(date '2017-01-01');
enddate:=(date '2017-12-31');
fiscalstartdate:=(date '2016-09-01');
-- Calculate date diff
daydiff:=make_interval(days => startdate - fiscalstartdate);
-- Insert records into table
INSERT INTO master.Calendar (
full_date
, year
, fiscal_year
)
SELECT
CAST(generate_series AS date) AS full_date
, EXTRACT(YEAR FROM generate_series)::INTEGER AS year
, EXTRACT(YEAR FROM generate_series + daydiff)::INTEGER AS fiscal_year
FROM (
SELECT generate_series(
startdate::timestamp,
enddate::timestamp,
interval '1 days'
)
) a;
END $$;
It does exactly what I want to do. However, I want to be able to call that in a function or stored procedure to automate rebuilding of tables and data. I then tried a few different things, but where I am at right now is this. Again, this is the trimmed down version, but still the issue is present.
CREATE OR REPLACE PROCEDURE create_calendar_table(startdate DATE, enddate DATE, fiscalstartdate DATE) AS $$
DECLARE
daydiff INTERVAL;
BEGIN
-- Calculate date diff
daydiff:=make_interval(days => startdate - fiscalstartdate);
-- Insert records into table
INSERT INTO master.Calendar (
full_date
, year
, fiscal_year
)
SELECT
CAST(generate_series AS date) AS full_date
, EXTRACT(YEAR FROM generate_series)::INTEGER AS year
, EXTRACT(YEAR FROM generate_series + daydiff)::INTEGER AS fiscal_year
FROM (
SELECT generate_series(
startdate::timestamp,
enddate::timestamp,
interval '1 days'
)
) a;
END $$ LANGUAGE SQL;
I receive an error of 'syntax error at or near "INTERVAL"'. I have tried a few things like trying to add a DO $$ in there and that did not seem to work well at all. Very well could be my ignorance on this version of SQL. The best I can tell is that I can't declare an INTERVAL type when creating a procedure or function like I can in the DO $$.
How can I get around this or what is the proper way of doing this in PostgreSQL? Any help is greatly appreciated.

You need language plpgsql - you can't use variables inside a SQL function.

Related

Add partition in existing table Greenplum

I am trying to add monthly partition on a table for an year or so. But the issue is I cannot add them in a single query. While creating the table in the past, I have added the partition for each month for couple of years.
CREATE TABLE Calls (
callid varchar(200) NOT NULL,
calltime timestamp NOT NULL,
Duration varchar(50) NULL
)
DISTRIBUTED BY (callid)
PARTITION BY RANGE(calltime)
(
START ('2019-04-01 00:00:00'::timestamp without time zone) END ('2022-01-01 00:00:00'::timestamp without time zone) EVERY ('1 mon'::interval)
I read different articles and blogs on it but could not found any solution to add monthly partitions for year or so. The only possible way is to add manually one by one for each month.
alter table Calls
Add partition
start (date '2022-01-01') inclusive
end (date '2022-02-01') exclusive
--And Again for next month
alter table Calls
Add partition
start (date '2022-02-01') inclusive
end (date '2022-03-01') exclusive
I have around 50 60 tables and doing it manually for each table will take a lot of time and effort. I am trying to make a generic way to add partitions. Any solution?
the quickest i have always found was a dirty perl script that looped over the different ranges and just pre-writes all the SQL something like this:
$SQL = "alter table Calls Add partition start (date 'START') inclusive end (date 'END') exclusive;";
for ($loop=<start>; $loop <= <end>; $loop+=<interval>)
{
$SQL =~ s/START/$loop/;
$SQL =~ s/END/END/;
print $SQL, "\n";
}
Hope that pseudo code helps
There is no direct way for adding multiple partitions without specifying range for each partition. It will simply through error like below.
gpadmin=# alter table calls add partition START ('2022-01-01 00:00:00'::timestamp without time zone) END ('2023-01-01 00:00:00'::timestamp without time zone) EVERY ('1 mon'::interval);
ERROR: cannot specify EVERY when adding RANGE partition to relation "calls"
gpadmin=#
You can create new table with different partition range then replace the Calls table with new table but it a resource consuming process and requires manual intervention.
Thanks,
Anil
Managed to create a dynamic function that takes datestart text,dateend text,table_schema text,table_name text as parameter and loops over to add a partition.
CREATE OR REPLACE FUNCTION partitionfunction(datestart text,dateend text,table_schema text,table_name text)
RETURNS void
LANGUAGE plpgsql
VOLATILE
AS $$
declare
var_Table_schema text;
var_Table_name text;
var_dateStart date;
var_dateEnd date;
var_endPartition date;
BEGIN
var_Table_schema := Table_schema;
var_Table_name := Table_name;
var_dateStart := to_date(dateStart,'YYYY-MM-DD');
var_dateEnd := to_date(dateEnd,'YYYY-MM-DD');
WHILE var_dateStart < var_dateEnd
loop
var_endPartition = var_dateStart + interval '1 MONTH'; --You can also change it for weeks or days
execute ' alter table '|| Table_schema ||'.'|| Table_name || '
add partition start('''||var_dateStart||''') end(
'''||var_endPartition||''')';
var_dateStart = var_dateStart + interval '1 MONTH'; --You can also change it for weeks or days
END loop;
end;
$$
EXECUTE ON ANY;

Capture values from a table and declare as a local variable in SQL Function

I'm learning how to write functions in SQL and this might be simple but can't seem to find what I'm looking for. I have a function as follows:
CREATE OR REPLACE FUNCTION A.aggregate(r_id text)
RETURNS VARCHAR
LANGUAGE plpgsql
AS $function$
begin
EXECUTE('CREATE UNLOGGED TABLE F.counts AS
SELECT name,
unique_cats,
unique_dogs,
total_cats,
total_dogs
FROM A.tb1
WHERE date in (select date from A.tb1) BETWEEN''' || start_date || '''AND''' || end_date ||
''';''');
END
$function$
;
I want to capture the minimum date and maximum date and use it in my execute function to filter out from my tb1 to get rows of data where the date is between the start and end date.
Modifications to your code:
-- Add to below
DECLARE
min_date date;
max_date date;
-- Add to below
BEGIN
SELECT INTO min_date, max_date min(some_date_fld), max(some_date_fld) FROM BA.activity_dates;
... BETWEEN $1 AND $2 ...
) USING min_date, max_date;
For SELECT INTO see SELECT INTO. For USING see Dynamic.

How do I store date Variable in Postgres function?

So I am working to create a function that will delete the 1 month worth records from a table. The table is in postgres. As postgres does not have stored procedures I am trying to declare a function with the logic that will insert the 1 month records into a history table and then delete the records from the live table. I have the following code :
CREATE FUNCTION DeleteAndInsertTransaction(Integer)
RETURNS Void
AS $Body$
SELECT now() into saveTime;
SELECT * INTO public.hist_table
FROM (select * from public.live_table
WHERE update < ((SELECT * FROM saveTime) - ($1::text || ' months')::interval)) as sub;
delete from public.live_table
where update < ((SELECT * FROM saveTime) - ($1::text || ' months')::interval);
DROP TABLE saveTime;
$Body$
Language 'sql';
So the above code compiles fine but when I try to run it by invoking it :- DeleteAndInsertTransaction(27) it gives me an
Error: relation "savetime" does not exist and I have no clue what is going on here.
If I take out the SELECT now() into saveTime; out of the function bloc and declare it before invoking the function then it runs fine but I need to store the current date into a variable and use that as a constant for the insert and delete and this is going against a huge table and there could be significant time difference between the insert and deletes. Any pointers as to what is going on here ?
select .. into .. is the deprecated syntax for create table ... as select ... which creates a new table.
So, SELECT now() into saveTime; actually creates a new table (named savetime), and is equivalent to: create table savetime as select now(); - it's not storing something in a variable.
To store a value in a variable, you need to first declare the variable, then you can assign the value. But you can only do that in PL/pgSQL, not SQL
CREATE FUNCTION DeleteAndInsertTransaction(p_num_months integer)
returns void
as
$Body$
declare
l_now timestamp;
begin
l_now := now();
...
end;
$body$
language plpgsql;
To insert into an existing table you need
insert into public.hist_table
select *
from public.live_table.
To select the rows from the last x month, there is no need to store the current date and time in a variable to begin with. It's also easier to use make_interval() to generate an interval based on a specified unit.
You can simply use
select *
from live_table
where updated_at <= current_date - make_interval(mons => p_pum_months);
And as you don't need a variable, you can actually do all that with a language sql function.
So the function would look something like this:
CREATE FUNCTION DeleteAndInsertTransaction(p_num_months integer)
RETURNS Void
AS
$Body$
insert into public.hist_table
select *
from live_table
where updated_at < current_date - make_interval(months => p_pum_months);
delete from public.live_table
where updated_at < current_date - make_interval(months => p_pum_months);
$Body$
Language sql;
Note that the language name is an identifier and should not be quoted.
You can actually do the DELETE and INSERT in a single statement:
with deleted as (
delete from public.live_table
where updated_at <= current_date - make_interval(months => p_pum_months)
returning *
)
insert into hist_table
select *
from deleted;

Trying to get current year in Oracle Apex

I have tried different SQL commands like Year(Getdate()) or sysdate, but none of them work.
What I'm trying to do is create a column that gets and stores the current year every time the form is filled out by the user. But for some reason the SQL code doesn't work on Oracle Apex (5.0).
DECLARE
CURRENT_YEAR VARCHAR2(5);
BEGIN
SELECT TO_CHAR(SYSDATE,'YYYY') INTO CURRENT_YEAR FROM DUAL;
RETURN CURRENT_YEAR;
END;
use the sql query and paste the item default in oracle apex page item, it will return the current year. not getting an error.
check the below image.
The oracle query to get the current year is:
SELECT EXTRACT( YEAR FROM SYSDATE ) FROM DUAL;
Expanding on the previous answer...
SELECT EXTRACT( YEAR FROM SYSDATE ) INTO yourVariable FROM DUAL;

Multiple Rows in Singleton Select

Yesterday, an anomaly occurred in our legacy software that I've never seen before. It triggers the following error:
multiple rows in singleton select At procedure 'POINTS_BALANCE'
Here is the Stored Procedure
CREATE PROCEDURE POINTS_BALANCE (
OPERATOR CHAR (3),
PERIOD VARCHAR (75))
RETURNS (
P_BALANCE INTEGER)
AS
DECLARE VARIABLE B_DATE timestamp;
DECLARE VARIABLE E_DATE timestamp;
DECLARE VARIABLE ALLOWED_POINTS INTEGER;
begin
P_BALANCE = NULL;
SELECT DATE_BEGIN, DATE_END, TOTAL_POINTS FROM SCHED_POINT_PERIODS
WHERE DESCRIPTION = :PERIOD INTO :B_DATE, :E_DATE, :ALLOWED_POINTS;
IF (B_DATE IS NULL) THEN
BEGIN
SELECT DATE_BEGIN, DATE_END, TOTAL_POINTS FROM SCHED_POINT_PERIODS
WHERE cast('NOW' as timestamp) BETWEEN DATE_BEGIN AND DATE_END+1 INTO :B_DATE, :E_DATE,
:ALLOWED_POINTS;
END
IF (B_DATE IS NOT NULL) THEN
BEGIN
E_DATE = E_DATE + 1;
SELECT SUM(POINTS)+:ALLOWED_POINTS FROM SCHED_ACTUAL
WHERE OPR = :OPERATOR AND BEGIN_TIME BETWEEN :B_DATE AND :E_DATE
INTO :P_BALANCE;
IF (P_BALANCE IS NULL) THEN
P_BALANCE = ALLOWED_POINTS;
END
SUSPEND;
end
SCHED_ACTUAL is a table that includes the check-in, check-out times of each user
SCHED_POINT_PERIODS is a table that holds the Allowed_Point values for each period (like Spring 2013, Fall 2013, Christmas Break 2013)
I'm not sure which one is a singleton. Is there a way I can tell just from this stored procedure?
SCHED_POINT_PERIODS are supposed to have non-overlapping periods. (i.e. they are supposed to be unique). This is the singleton that the error was referring to.
I noticed this overlap via the DB and fixed this internally. The error is resolved.