I am trying to convert an oracle stored procedure to Postgres function/procedure.
I did some research and read many forums to prepare the syntax for the stored procedure in postgres.
But getting an error for declaring an integer variable.
My code is like below:
The purpose of my procedure is to load one-month records into another month (EX: load Jan 2020 data into March 2020)
Postgres Procedure:
CREATE OR REPLACE FUNCTION Corporate.copy_forecast(code OUT integer, message OUT VARCHAR)
LANGUAGE plpgsql
AS $$
DECLARE
v_current_month integer;
v_previous_month integer;
begin
select max(cycleid) into v_previous_month, max(cycleid)+1 into v_current_month from Corporate.forecast;
INSERT INTO Corporate.forecast
(SELECT v_current_month,lob,delivery,forecast_val
FROM Corporate.forecast
WHERE month= v_previous_month);
code:=1;
message:='Sucussfully loaded previous month forecast to current month';
exception
when others then
code:=0;
message:='Failed';
END;
$$
Please help me to fix the above procedure.
There are several problems with your code:
The first one is simple: you say LANGUAGE sql, but you write PL/pgSQL code.
That explains the error message you get. Use LANGUAGE plpgsql if you want to write PL/pgSQL.
You are using variables that are the same as column names, which leads to ambiguity. For example, you declare
current_month integer;
previous_month integer;
but you have a WHERE clause
WHERE current_month = previous_month
where obviously one of them should refer to a variable and the other to a table column. That will not work and cause errors.
The best and simplest solution is to always use variable names that are different from column names. A simple method is to start all variable names with v_.
A second option is to always qualify columns with the table name and variables with the function name.
You do not have a problem declaring an integer variable it is much larger you have structural problems. Below I indicate those issues and will then show corrections. The indicator "--<< ..." discusses the line(s) just above it.
CREATE OR REPLACE FUNCTION copy_forecast(code OUT integer, message OUT VARCHAR)
--<< Improper format Should be (out code integer, out message varchar)
--<< and while not invalid IMHO bad design a function/procedure should just do its job correctly or raise an exception
returns ???
--<< This is missing. A Postgres Function MUST declare what it returns, if nothing then RETURNS VOID. But I guess this was an Oracle Procedure.
LANGUAGE plpgsql
AS $$
DECLARE
v_current_month integer;
v_previous_month integer;
begin
select max(cycleid) into v_previous_month, max(cycleid)+1 into v_current_month from forecast;
--<< Invalid format Should Be select var1,var2 into local1, local2 ...
INSERT INTO forecast
--<< Very dangerous. If table ever changes this will fail.
(SELECT v_current_month,lob,delivery,forecast_val
FROM forecast
WHERE month= v_previous_month);
code:=1;
message:='Successfully loaded previous month forecast to current month';
exception
when others then
code:=0;
message:='Failed'
--<< Very dangerous (all 3 lines). When an error occurs you will never know what it is. See insert above
;
END;
$$;
The following corrects the errors indicated above. Again the indicator (--*) discussed the line(s) above it.
create or replace function copy_forecast()
returns boolean
language plpgsql
as $$
declare
v_current_month integer;
v_previous_month integer;
begin
select max(cycleid)
, max(cycleid)+1
into v_previous_month
, v_current_month
from forecast;
insert into forecast (cycleid,lob,delivery,forcast_val )
select v_current_month,lob,delivery,forecast_val
from forecast
where month= v_previous_month);
--* insert will fail I cannot resolve. MONTH not inserted, but must exist on table.
--* It seems you are using cycleid and month as synonyms
return True;
--* you can leave the out parameters if desirded. Set them before the return
exception
when others then
-- Log the error and debug information here
return false;
--* you can leave out parameters if desired Set them before the return.
end;
This should correct the structure issues with your function. However, the logical issue with month/cycleid remains.
However, this does not actually get you to your goal. As stated "the purpose of my procedure is to load one-month records into another month (EX: load Jan 2020 data into March 2020)". This function CANNOT do that. It can only copy the latest month/cycleid to the next month/cycleid; so Jan->Feb, Feb->Mar, ... Nov->Dec. But Dec->Jan would fail unless month/cycleid can be 13, and subsequent months would continue increasing month/cycle. Accomplishing your goal required INPUT parameters for the Source(from) and Target(to). And as #LaurenzAlbe said "no moving targets". That would be another question.
Related
I have Postgres function that needs to iterate on an ARRAY of tables_name and should save the value that will be returned from the query each time to array.
maybe this is not correct way so if there is better ways to do it I'll be glad to know :)
I've try with format function to generate different queries each time.
CREATE OR REPLACE FUNCTION array_iter(tables_name text[],idd integer)
RETURNS void
LANGUAGE 'plpgsql'
AS $BODY$
declare
current_table text;
current_height integer :=0;
quer text;
heights integer[];
begin
FOREACH current_table IN ARRAY $1
LOOP
quer:=format('SELECT height FROM %s WHERE %s.idd=$2', current_table);
current_height:=EXECUTE quer;
SELECT array_append(heights, current_height);
END LOOP;
RAISE NOTICE '%', heights;
end;
$BODY$;
First off you desperately need to update your Postgres as version 9.1 is no longer supported and has not been for 5 years (Oct 2016). I would suggest going to v13 as it is the latest, but an absolute minimum to 10.12. That will still has slightly over a year (Nov 2022) before it looses support. So with that in mind.
The statement quer:=format('SELECT height FROM %s WHERE %s.idd=$2', current_table); is invalid, it contains 2 format specifiers but only 1 argument. You could use the single argument by including the argument number on each specifier. So quer:=format('SELECT height FROM %1s WHERE %1s.idd=$2', current_table);. But that is not necessary as the 2nd is a table alias which need not be the table name and since you only have 1 table is not needed at all. I would however move the parameter ($2) out of the select and use a format specifiers/argument for it.
The statement current_height:=EXECUTE quer; is likewise invalid, you cannot make the Right Val of assignment a select. For this you use the INTO option which follows the statement. execute query into ....
While SELECT array_append(heights, current_height); is a valid statement a simple assignment heights = heights || current_height; seems easier (at least imho).
Finally there a a couple omissions. Prior to running a dynamic SQL statement it is good practice to 'print' or log the statement before executing. What happens when the statement has an error. And why build a function to do all this work just to throw away the results, so instead of void return integer array (integer[]).
So we arrive at:
create or replace function array_iter(tables_name text[],idd integer)
returns integer[]
language plpgsql
as $$
declare
current_table text;
current_height integer :=0;
quer text;
heights integer[];
begin
foreach current_table in array tables_name
loop
quer:=format('select height from %I where id=%s', current_table,idd);
raise notice 'Running Query:: %',quer;
execute quer into current_height;
heights = heights || current_height;
end loop;
raise notice '%', heights;
return heights;
exception
when others then
raise notice 'Query failed:: SqlState:%, ErrorMessage:%', sqlstate,sqlerrm;
raise;
end;
$$;
This does run on version as old as 9.5 (see fiddle) although I cannot say that it runs on the even older 9.1.
This covers most use cases How do you use variables in a simple PostgreSQL script? but not the select clause.
This code produces an error column "ct" does not exist"
DO
$$
declare CT timestamp := '2020-09-04 23:59:59';
select CT,5 from job;
$$;
I can see why it would interpret CT as a column name. What's the Postgres syntax required to refer to a variable in the context of the select clause?
I would expect that query to return
'2020-09-04 23:59:59',5
for each row in the job table.
Addendum to the accepted answer
My use case doesn't return rows. Instead, the result of the select is consumed by an insert statement. I'm transforming rows from staging tables into other tables and adding value like the import date and the identity owning the inserts. It's these values that are provided by the variables - they are used in several such transforms and the point of the variable is to let me set each value once up the top of the script.
Because the rows are consumed like this, it turns out that I don't need a function wrapping this code. It's a bit inconvenient to test since I can't run the select and look at the outcome without copying it and pasting in literals, but at least it's possible to use variables. My working script looks like this:
do
$$
declare ct timestamp := '2020-09-04 23:59:59';
declare cb int := 2;
declare iso8601 varchar(50) := 'YYYY-MM-DD HH24:MI:SS';
declare USAdate varchar(50) := 'MM-DD-YYYY HH24:MI:SS';
begin
delete from dozer_wheel_loader_equipment_movement where created = ct;
INSERT INTO dozer_wheel_loader_equipment_movement
(site, primary_category_id, machine, machine_class, x, y, z, timestamp_local, created, created_by)
select site ,mc.id ,machine , machineclass ,x,y,z,to_timestamp(timestamplocal, iso8601), ct, cb
from stage_dozer_csv d join machine_category mc on d.primarycategory = mc.short_code;
...
end
$$
There is a lot of worthwhile related reading at How to declare a variable in a PostgreSQL query
There are few things about variables in PostgreSQL.
Variable can not be used in Plain SQL in Postgres. So you have to use any pl language i.e. plpgsql to use this. You have tried the same in your example.
In your DO block you have missed the Begin and End, So you have to write it like below
DO
$$
begin
declare CT timestamp := '2020-09-04 23:59:59';
select CT,5 from job;
end
$$;
But when you read the official documentation of DO Statement, it says DO will allow to run the anonymous code but it returns void, that's why above code will throw following error:
ERROR: query has no destination for result data
HINT: If you want to discard the results of a SELECT, use PERFORM instead.
CONTEXT: PL/pgSQL function inline_code_block line 4 at SQL statement
So there is only one way - wrap this code block in a Function like below:
create or replace function func() returns table(col1 timestamp, col2 int )
AS
$$
declare ct timestamp := '2020-09-04 23:59:59';
begin
return query
select CT,5 from job;
end;
$$
language plpgsql
and you can call it like below:
select * from func()
DEMO
Conclusion
You can not use variable in normal SQL statement in Postgres.
You have to use any Procedural Language i.e. plpgsql to use variable.
DO Block doesn't return any value so you can not use select statement like above in DO block. It is good for non-returning queries i.e. insert, update, delete or grant etc.
Only way to return a value from procedural language code block is - you have to wrap it in a suitable PostgreSQL Function.
I am trying to create a trigger that on update of one table, runs a query and updates another table with the results.
Where I am getting stuck, is assigning the result of the query to a correctly typed variable.
The current error is that the array must start with "{" or other dimensional information however as I make tweaks I get other errors
Please see my current code below and let me know the best approach
Your help is very appreciated as I have spent a huge amount of time consulting google.
CREATE TYPE compfoo AS (ownership character varying (50), count INT);
CREATE OR REPLACE FUNCTION test1_update() RETURNS trigger AS
$$
DECLARE
largest_owner character varying (50);
temp_result compfoo[];
BEGIN
SELECT ownership, count(*) INTO temp_result
FROM austpoly2
WHERE ownership IS NOT NULL
group by ownership
ORDER BY count DESC
LIMIT 1;
largest_owner = temp_result[0].ownership;
UPDATE public.states
SET ownership= largest_owner
WHERE statecode='1';
RETURN NEW;
END;
$$
LANGUAGE plpgsql;
CREATE TRIGGER test1_update_trigger
BEFORE UPDATE ON austpoly2
FOR EACH ROW EXECUTE PROCEDURE test1_update();
Thankyou a_horse with_no_name
Your response combined with
temp_result compfoo%ROWTYPE;
solved this problem
Edit: After posting I found Erwin Brandstetter's answer to a similar question. It sounds like in 9.2+ I could use the last option he listed, but none of the other alternatives sound workable for my situation. However, the comment from Jakub Kania and reiterated by Craig Ringer suggesting I use COPY, or \copy, in psql appears to solve my problem.
My goal is to get the results of executing a dynamically created query into a text file.
The names and number of columns are unknown; the query generated at run time is a 'pivot' one, and the names of columns in the SELECT list are taken from values stored in the database.
What I envision is being able, from the command line to run:
$ psql -o "myfile.txt" -c "EXECUTE mySQLGeneratingFuntion(param1, param2)"
But what I'm finding is that I can't get results from an EXECUTEd query unless I know the number of columns and their types that are in the results of the query.
create or replace function carrier_eligibility.createSQL() returns varchar AS
$$
begin
return 'SELECT * FROM carrier_eligibility.rule_result';
-- actual procedure writes a pivot query whose columns aren't known til run time
end
$$ language plpgsql
create or replace function carrier_eligibility.RunSQL() returns setof record AS
$$
begin
return query EXECUTE carrier_eligibility.createSQL();
end
$$ language plpgsql
-- this works, but I want to be able to get the results into a text file without knowing
-- the number of columns
select * from carrier_eligibility.RunSQL() AS (id int, uh varchar, duh varchar, what varchar)
Using psql isn't a requirement. I just want to get the results of the query into a text file, with the column names in the first row.
What format of a text file do you want? Something like csv?
How about something like this:
CREATE OR REPLACE FUNCTION sql_to_csv(in_sql text) returns setof text
SECURITY INVOKER -- CRITICAL DO NOT CHANGE THIS TO SECURITY DEFINER
LANGUAGE PLPGSQL AS
$$
DECLARE t_row RECORD;
t_out text;
BEGIN
FOR t_row IN EXECUTE in_sql LOOP
t_out := t_row::text;
t_out := regexp_replace(regexp_replace(t_out, E'^\\(', ''), E'\\)$', '');
return next t_out;
END LOOP;
END;
$$;
This should create properly quoted csv strings without the header. Embedded newlines may be a problem but you could write a quick Perl script to connect and write the data or something.
Note this presumes that the tuple structure (parenthesized csv) does not change with future versions, but it currently should work with 8.4 at least through 9.2.
I can't run this block in PostgreSQL 8.2.
DECLARE
curtime char;
BEGIN
curtime := 'now';
INSERT INTO logtable VALUES (logtxt, curtime);
RETURN curtime;
END;
When I try it shows the error:
ERROR: syntax error at or near "char"
SQL state: 42601
It sounds like you're trying to run a PL/PgSQL code block stand-alone, without wrapping it up in a function using CREATE OR REPLACE FUNCTION. That won't work, you need to include it in a function or (from PostgreSQL 9.0) a DO block. PL/PgSQL and plain SQL are different languages so you can't just run PL/PgSQL code directly.
It'd help if you explained why you're trying to write the code you pasted. I suspect you're trying to solve a problem that's better handled with a trigger function like an audit trigger.
Some important notes:
You need to update PostgreSQL: PostgreSQL 8.2 is dangerously out of date and unsupported. security and bug fixes are no longer being released. Upgrade urgently to a supported version, but make sure to read the release notes for each major ".0" version like "8.3.0", "8.4.0", etc for migration and compatibility advice.
Avoid 'now': Also, instead of using 'now' you should usually use the current date/time functions, particularly current_timestamp.
current_timestamp is stable: The hoop-jumping you are doing is probably unnecessary because the value of current_timestamp (and 'now'::timestamp) doesn't change for the duration of a transaction. Eg:
regress=# BEGIN;
regress=# SELECT current_timestamp;
2012-08-14 14:52:43.382596+08
regress=# SELECT pg_sleep(5);
regress=# SELECT current_timestamp;
2012-08-14 14:52:43.382596+08
Details
Your intention appears to be something like the following (incorrect, do not use) code:
CREATE OR REPLACE FUNCTION some_function(logtxt text) RETURNS timestamptz AS $$
DECLARE
curtime char;
BEGIN
curtime := 'now';
INSERT INTO logtable VALUES (logtxt, curtime);
RETURN curtime;
END;
$$ LANGUAGE 'plpgsql';
but you've misused the char datatype, which requires a length parameter. It defaults to 1 if not supplied so you'll get:
regress=# SELECT some_function();
ERROR: value too long for type character(1)
CONTEXT: PL/pgSQL function "some_function" line 5 at assignment
NEVER use the char datatype in SQL; use varchar or text. For cross-database portability varchar(n) where n is a maximum length is required; if portability isn't needed use text.
If you change char to text in the above, your code might run, but it still doesn't make any sense. I strongly suspect that you really want to write:
CREATE OR REPLACE FUNCTION some_function(logtxt text) RETURNS timestamptz AS $$
BEGIN
INSERT INTO logtable VALUES (logtxt, current_timestamp);
RETURN current_timestamp;
END;
$$ LANGUAGE 'plpgsql';
... but you didn't know about the current date/time functions.
Even that's too much, really. I think you're trying to solve a problem that's a better fit for a trigger.