Is there a way to fetch rows using cursors in PostgresSQL? - sql

This is my first time learning cursors. I have two tables: drink d and cart c. They both contain a common primary key where c.did = d.did. (where did = drink id). I'm trying to take the teatype from joining both rows so that I can update the stock of the teatype. For example, if it returns:
-----------
| teatype|
-----------
Oolong
Green Tea
Oolong
Then I would loop through that record so that I can update the stock of that specific teatype in the other table tea which contains the columns, stock where the teatype = the teatype of the cursor.
The issue I'm running into is not knowing how to correctly implement the cursor so that it can get looped through to update the stock. I've run into issues saying theres a syntax error ar or near end or at if. I've looked all over the web but can't find the correct resources.
I've tried putting the procedural statements within a function but that didn't help. It seems like I'm not on the right track and don't know how to approach this problem. My code is the following and I'm trying to run it in a PostgresSQL server.
CREATE OR REPLACE FUNCTION foo()
RETURNS VOID AS $$
DECLARE
tea_type RECORD;
TeaCursor CURSOR FOR
SELECT teatype FROM cart c
JOIN drink d ON c.did = d.did;
Begin
OPEN TeaCursor;
LOOP
FETCH from TeaCursor into tea_type;
exit when tea_type = null;
UPDATE tea t
SET stock = stock - 1
WHERE t.type = tea_type.teatype;
end if;
END LOOP;
close TeaCursor;
End;
$$LANGUAGE sql stable;
I should expect something like UPDATE 1 or something that would say it has succesfully decremented the stock for that specific teatype. Instead I keep getting an error as such:
psql:test.sql:27: ERROR: syntax error at or near "RECORD"
LINE 5: tea_type RECORD;

You declared the function as LANGUAGE sql when really it is LANGUAGE plpgsql.

Related

Change the parameter each time and run the sql script

I am quite new to sql and have been trying to work on the following script to parameterize it.
This is my code:
select
dc.deviceid,
dc.kernel_time,
dc.crash_time,
dc.crash_process,
dps.start_time,
dps.end_time,
dps.start_kernel_time,
dps.end_kernel_time,
case
when dc.kernel_time between dps.start_kernel_time and dps.end_kernel_time then 1
when dc.crash_time between dps.start_time and dps.end_time then 2
else 3
end as flag,
ROW_NUMBER () over (partition by dc.deviceid, dc.kernel_time,
dc.crash_time, dc.crash_process order by flag) row_num
from dummy.dummy_crashes dc
left outer join (select *
from dummy.dummy_power) as dps
on dc.deviceid = dps.deviceid
and ((dc.kernel_time between (dps.start_kernel_time + 10000) and (dps.end_kernel_time + 10000)) or (dc.crash_time between dps.start_time and dps.end_time))
order by dc.crash_time;
I need to test this script by changing the start_kernel_time and end_kernel_time with a certain int parameter value (in this example shown: 10000) every time. So, instead of modifying it in the code, I would like to create a function with the int parameter of choice and run this script. Would that be possible?
I am really clueless as to how to achieve that.
My ideal idea would be something like this:
get_crashes(10000); <-- get records with adding int parameter (in start_kernel_time and end_kernel_time) as 10000
get_crashes(30000); <-- get records with adding int parameter as 30000
get_crashes(80000); <-- get records with adding int parameter as 80000
I am really trying to understand how I could achieve this?
I can't write a comment because i don't have 50rep, but here is my answer:
You can create a temp table with values that you want to pass and call cursor with simple query like:
SELECT [value] FROM *temptable*
After that, inside cursor just write script with single value from above temp table
UPDATE
DECLARE
cur CURSOR FOR select col1 from tempTable;
test_cur RECORD;
BEGIN
open cur;
LOOP
fetch cur into test_cur;
exit when test_cur = null;
if test_cur.col1 IS NOT NULL then
return next test_cur.col1;
end if;
END LOOP;
close cur;
END;
One note - I never write PostgreSQL, just have knowledge about SQL and find code on internet, so maybe you need to check documentation.

SQL Oracle - Procedure Syntax (School assignment)

I'm currently learning SQL and I'm having trouble with a procedure of mine. The procedure should calculate the average of a column called 'INDICE_METABO_PAT'. The information I need is in 3-4 different tables. Then when I do have the average calculated, I update a table to set this average to the corresponding entries. Here is the procedure. Note that everything else in my .sql file works : inserts, updates, selects, views, etc.
create or replace Procedure SP_UPDATE_INDICE_METABO_DV (P_NO_ETUDE in number)
is
V_SOMME number := 0;
V_NBPATIENT number := 0;
V_NO_ETUDE number := P_NO_ETUDE;
cursor curseur is
select PATIENT.INDICE_EFFICACITE_METABO_PAT
from ETUDE, DROGUE_VARIANT, ETUDE_PATIENT, PATIENT
where ETUDE.NO_DROGUE = DROGUE_VARIANT.NO_DROGUE
and ETUDE.NO_VAR_GEN = DROGUE_VARIANT.NO_VAR_GEN
and V_NO_ETUDE = ETUDE_PATIENT.NO_ETUDE
and ETUDE_PATIENT.NO_PATIENT = PATIENT.NO_PATIENT;
begin
open curseur;
fetch curseur into V_SOMME;
V_NBPATIENT := V_NBPATIENT + 1;
exit when curseur%NOTFOUND;
update DROGUE_VARIANT
set INDICE_EFFICACITE_METABO_DV = V_SOMME / V_NBPATIENT
where exists(select * from ETUDE, DROGUE_VARIANT, ETUDE_PATIENT, PATIENT
where ETUDE.NO_DROGUE = DROGUE_VARIANT.NO_DROGUE
and ETUDE.NO_VAR_GEN = DROGUE_VARIANT.NO_VAR_GEN
and V_NO_ETUDE = ETUDE_PATIENT.NO_ETUDE
and ETUDE_PATIENT.NO_PATIENT = PATIENT.NO_PATIENT);
end SP_UPDATE_INDICE_METABO_DV;
/
I'm getting an error : Procedure compiled , error check compiler log.
But I can't open the compiler log, and when my friend opens it, it points to weird places, like my create tables and such.
This is school stuff by the way, so it'll be nice if you could give an insight instead of a direct solution. Thanks alot.
Thanks alot in advance for your kind help !
To see the error you can do show errors after your procedure creation statement, or you can query the user_errors or all_errors views.
That will show something like:
LINE/COL ERROR
-------- ------------------------------------------------------------------------
20/4 PLS-00376: illegal EXIT/CONTINUE statement; it must appear inside a loop
20/4 PL/SQL: Statement ignored
You mentioned that when you checked the complier log, which shows the same information, "it points to weird places". Presumably you're looking at line 20 in your script. But this message is referring to line 20 of the PL/SQL code block, which is the exit when curseur%NOTFOUND;, which makes sense for the error message.
And as the message also says, and as #ammoQ said in a comment, that should be in a loop. If you're trying to manually calculate the average in a procedure as an exercise, instead of using the built-in aggregation functions, then you need to loop over all of the rows from your cursor:
open curseur;
loop
fetch curseur into V_SOMME;
exit when curseur%NOTFOUND;
V_NBPATIENT := V_NBPATIENT + 1;
end loop;
close curseur;
But as you'll quickly realise, you'll end up with the v_somme variable having the last value retrieved, not the sum of all the values. You need a separate variable keep to track of the sum - fetch each value into a variable, and add that to your running total, all within the loop. But as requested, not giving a complete solution.
As you're starting out you should really use ANSI join syntax, not the old from/where syntax you have now. It's a shame that is still being taught. So your cursor query should be something like:
select PATIENT.INDICE_EFFICACITE_METABO_PAT
from ETUDE_PATIENT
join ETUDE
-- missing on clause !
join DROGUE_VARIANT
on DROGUE_VARIANT.NO_DROGUE = ETUDE.NO_DROGUE
and DROGUE_VARIANT.NO_VAR_GEN = ETUDE.NO_VAR_GEN
join PATIENT
on PATIENT.NO_PATIENT = ETUDE_PATIENT.NO_PATIENT
where ETUDE_PATIENT.NO_ETUDE = P_NO_ETUDE;
... which shows you that you are missing a join condition between ETUDE_PATIENT and ETUDE - it's unlikely you want a cartesian product, and it's much easier to spot that missing join using this syntax than with what you had.
You need to look at your update statement carefully too, particularly the exists clause. That will basically always return true if the cursor found anything, so it will update every row in DROGUE_VARIANT with your calculated average, which presumably isn't what you want.
There is no correlation between the rows in the table you're updating and the subquery in that clause - the DROGUE_VARIANT in the subquery is independent of the DROGUE_VARIANT you're updating. By which I mean, it's the same table, obviously; but the update and the subquery are looking at the table separately and so are looking at different rows. It also has the same missing join condition as the cursor query.

PL/pgSQL Return SETOF Records Error

I am relatively new to postgresql and battling my way to get familiarized with it. I had run in to an error while writing a new pl/sql function. ERROR: type "ordered_parts" does not exist
CREATE OR REPLACE FUNCTION get_ordered_parts(var_bill_to integer)
RETURNS SETOF ordered_parts AS
$BODY$
declare
var_ordered_id record;
var_part ordered_parts;
begin
for var_ordered in select order_id from view_orders where bill_to = var_bill_to
loop
for var_part select os.po_num,os.received,os.customer_note,orders.part_num,orders.description,orders.order_id,orders.remaining_quantity from (select vli.part_num,vli.description,vli.order_id,vli.quantity - vli.quantity_shipped as remaining_quantity from view_line_items as vli where vli.order_id in (select order_id from view_orders where bill_to = var_bill_to and order_id = var_ordered.order_id) and vli.quantity - vli.quantity_shipped > 0)as orders left join order_sales as os on orders.order_id = os.order_id
then
-- Then we've found a leaf part
return next var_part;
end if;
end loop;
end;
$BODY$
LANGUAGE 'plpgsql' VOLATILE
COST 100
ROWS 1000;
ALTER FUNCTION get_ordered_parts(integer) OWNER TO postgres;
just note - your code is perfect example how don't write stored procedure ever. For some longer results it can be extremely slow. Minimally two cycles can be joined to one, or better, you can use just only one RETURN QUERY statement. Next issue is zero formatting of embedded SQL - good length of line is between 70 and 100 chars - writing long SQL statement to one line going to zero readability and maintainability code.
Relation database is not array, and any query has some cost, so don't use nested FOR if you really don't need it. I am sorry for offtopic.
The error message is telling you that you have declared the return type of your function to be SETOF ordered_parts, but it doesn't know what kind of thing ordered_parts is. Within your Declare block you also have a variable declared as this same type (var_part ordered_parts).
If you had a table or view called ordered_parts, then its "row type" would be automatically created as a type, but this is not the case here. if you just want to use an arbitrary row from a result set, you can just use the generic type record.
So in this case your function should say RETURNS SETOF record, and your Declare block var_part record.
Bonus tip: rather than looping over the result of your query and running RETURN NEXT on each row, you can use RETURN QUERY to throw the whole result set into the returned set in one go. See this Postgres manual page.

How would you enforce DRY (Don't Repeat Yourself) in a SQL script?

I'm changing a database (oracle) with a script containing a few updates looking like:
UPDATE customer
SET status = REPLACE(status, 'X_Y', 'xy')
WHERE status LIKE '%X_Y%'
AND category_id IN
(SELECT id
FROM category
WHERE code = 'ABC');
UPDATE customer
SET status = REPLACE(status, 'X_Z', 'xz')
WHERE status LIKE '%X_Z%'
AND category_id IN
(SELECT id
FROM category
WHERE code = 'ABC');
-- More updates looking the same...
In this case, how would you enforce DRY (Don't Repeat Yourself)?
I'd particularly interested in solving the two following recurring problems:
Define a function, available from this script only, to extract the subquery SELECT id FROM category WHERE code = 'ABC'
Create a set of replace rules (that could look like {"X_Y": "yx", "X_Z": "xz", ...} in a popular programming language) and then iterate a single update query on it.
Thanks!
I would reduce it to a single query:
UPDATE customer
SET status = REPLACE(REPLACE(status, 'X_Y', 'xy'), 'X_Z', 'xz')
WHERE status REGEXP_LIKE 'X_[YZ]'
AND category_id IN
(SELECT id
FROM category
WHERE code = 'ABC');
First of all, remember that scripting is not the same thing as programming, and you don't have to adhere to DRY principles. Scripts like this one are usually one-offs, not a program to be maintained over a long time.
But you could use PL/SQL to do this:
declare
type str_tab is table of varchar2(30) index by binary_integer;
from_tab str_tab;
to_tab str_tab;
begin
from_tab(1) := 'X_Y';
from_tab(2) := 'X_Z';
to_tab(1) := 'xy';
to_tab(2) := 'xz';
for i in 1..from_tab.count loop
UPDATE customer
SET status = REPLACE(status, from_tab(i), to_tab(i))
WHERE status LIKE '%' || from_tab(i) || '%'
AND category_id IN
(SELECT id
FROM category
WHERE code = 'ABC');
end loop;
end;
Pretty straightforward, unless I'm missing something.
UPDATE customer
SET status = REPLACE(REPLACE(status,'X_Y','xy'),'X_Z','xz')
WHERE ( status LIKE '%X_Y%' Or status LIKE '%X_Z%')
AND category_id IN
(SELECT id
FROM category
WHERE code = 'ABC');
Write a script that takes parameters and call it multiple times. (I'm assuming you're using SQLPlus to run the script.)
replace_in_status.sql:
UPDATE customer
SET status = REPLACE(status, UPPER('&1'), '&2')
WHERE status LIKE '%' ||UPPER('&1')|| '%'
AND category_id IN
(SELECT id
FROM category
WHERE code = 'ABC');
Calling script:
#replace_in_status X_Y xy
#replace_in_status X_Z xz
Okay, a shot from the hip here, take it easy on my syntax :-)
Would an approach like this help:
DECLARE
v_sql1 VARCHAR2(1000);
v_sql2 VARCHAR2(2000);
TYPE T_Rules IS RECORD (srch VARCHAR2(100), repl(VARCHAR2(100));
TYPE T_RuleTab IS TABLE OF T_Rules INDEX BY BINARY_INTEGER;
v_rules T_RuleTab;
FUNCTION get_subquery RETURN VARCHAR2 IS
BEGIN
RETURN '(SELECT id FROM category WHERE code = ''ABC'')';
END;
BEGIN
v_sql1 := 'UPDATE customer SET status = REPLACE('':1'','':2'') WHERE status LIKE ''%:1%'' AND category_id IN ';
v_rules(1).srch := ('X_Y'); v_rules(1).repl := 'yx';
v_rules(2).srch := ('X_Z'); v_rules(2).repl := 'xz';
FOR i IN 1..v_rules.COUNT LOOP
v_sql2 := v_sql1||get_subquery();
EXECUTE IMMEDIATE v_sql2 USING v_rules(i).srch, v_rules(i).repl;
END LOOP;
END;
You could replace the PL/SQL table with a real table and run a cursor over it, but this addresses your second requirement.
Obviously some work is left on get_subquery, your first requirement ;-)
EDIT
Dang! forgot to mention you need to be careful with that replace string in your WHERE clause - underscores are a single character matching wild card in Oracle...
Depending on how important the script is, I would:
Just copy and paste and modify, or
Write a script in another programming language that has better ways to resolve the duplication.
For the replace rules you could create a temporary table and fill it with these replace rules, and then join with this table.
If the subquery is always the same, you have solved the first problem also by using a join.
I've seen a few approaches to this:
Use string buffers to assemble the sql dynamically using PL/SQL or in your programming language.
Use a framework such as IBATIS which let's you reuse and extend fragments of SQL that are stored in XML files.
Using an ORM framework circumvents this issue by working with objects rather than directly with the SQL.
Depending on your language and problem at hand using a framework may be the best approach and then extending it to do what you want it to do.
The solution suggested by soulmerge is the simplest, and therefore best one - you just need to nest the calls to "replace". I just want to add that the condition
status like '%tagada%'
is useless. replace() will change nothing to the status if the searched string is not found, therefore you can safely apply it to all rows. And since a condition where you search a string lost in the middle of another string cannot make any use of whatever index you have, it's useless as a filtering condition.
Your only filtering condition is the one on category_id ...
Which brings one point that justifies why soulmerge's solution is best: iterating on all the changes is a bad idea. Suppose that the filter on category_id is moderately selective, odds are that Oracle will choose to scan the table. Do you really want to scan the table each time when you can do all the changes in a single pass?

oracle trigger after inserting or updating a sales item

I have this table that represents a weak entity and is a typical table for introducing items ordered:
insert into ITEM_FORNECIMENTO values (a_orderId,a_prodId,a_prodQtd, a_buyPrice);
I want my trigger to update the last column (the total price of products WITHOUT iva) to do this : totalPrice= totalPrice*(1+(iva/100), each time I insert or update an entry on that table.
so, I came up with this, but I'm totally wrong when it comes to work with new and old values.
create or replace
trigger t_replaceTotal
after insert or update of id_prod,qtd_if,prec_total_if on item_fornecimento
for each row
declare
iva produto.iva_prod%type;
idProd produto.id_prod%type;
r_old item_fornecimento.prec_total_if%type:=null;
r_new item_fornecimento.prec_total_if%type:=null;
begin
select iva_prod,id_prod into iva,idprod from produto p where p.id_prod = id_prod;
r_old:= :old.prec_total_if;
r_new:= :new.prec_total_if;
update item_fornecimento item set prec_total_if = r_old * (1+(iva/100)) where item.id_prod = idprod;
end;
Could someone please help rewriting this code? I'm getting the error: ORA-01422: exact fetch returns more than requested number of rows
ORA-06512: at "FUSION.T_REPLACETOTAL", line 8
ORA-04088: error during execution of trigger 'FUSION.T_REPLACETOTAL'
The problem is that you are trying to fetch every row from the table because you are matching a column (id_prod) to itself. I suspect you want to use :new.id_prod or :old.id_prod.
select .... from produto p where p.id_prod = id_prod;
Next, make this a BEFORE INSERT/UPDATE trigger and replace the UPDATE statement with
:new.prec_total_if := r_old * (1+(iva/100));
Otherwise you'll get a mess of mutating table errors.
That error is telling you that a query that saves its result into a variable is returning more than one result. As such it doesn't know what you want to save in the variable.
Try running the following:
select iva_prod,id_prod from produto p where p.id_prod = id_prod;
and I bet it will give you more than one result, which it can't save into iva,idprod.