postgres function for loop - sql

I have an orders table and an order_products_items table.
The order_products_items has these fields:
order_id
product_id
quantity
price
I am trying to create a calculated_field: calculated_total_products_price in the orders table through a before insert trigger function that would calculate the total order price by looping through all the order_products_items related to the order and multiplying the quantity with the price for every order item.
This is my failed attempt in doing so:
CREATE OR REPLACE FUNCTION public.fn_trigger_total_order_price()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
declare total float := 0.0;
product record;
BEGIN
FOR product IN
SELECT itm.price, itm.quantity
FROM order_products_items itm
INNER JOIN orders ord
ON itm.order_id = ord.id
WHERE ord.id = NEW.id
LOOP
total = total + (product.price * product.quantity);
END LOOP;
NEW.calculated_total_products_price := total;
RETURN NEW;
END;
$function$
;
The trigger looks like this:
CREATE TRIGGER fn_trigger_total_order_price BEFORE INSERT ON public.orders
FOR EACH ROW EXECUTE PROCEDURE fn_trigger_total_order_price();
Somehow, I do not get any errors, but always get 0 as a result.
Am I missing something? or is there a better/more efficient way of approaching this?
Many thanks.

You don't need a loop in postgres to achieve this.
A simple AGGREGATION query would do.
Something like
SELECT SUM(quantity * price)
FROM order_products_items
WHERE order_id = NEW.id
You also don't really need to mess with a trigger if you don't want to.
Just make a view out of the query below and relate it to the orders table as something like order_totals
SELECT order_id, SUM(quantity * price) order_total
FROM order_products_items
GROUP BY order_id

Related

Trigger after insert with nested if conditions pl sql

I want to create a trigger after there's an insert in 'order_items' table. The trigger is to minus the quantity of that product id in 'inventories' table by the quantity being insert to 'order_items' table. If the result of quantity after being subtracted is negative, then the quantity of 'inventories' will be changed back to the old quantity and the data in 'order_items' will be deleted (the insertion is failed). If the product id is in many warehouses, then just minus the quantity with the smallest warehouse_id
Example #1:
Product ID '101' is available in Warehouse ID '1','2','3'
So choose the product id '101' in warehouse id '1' (smallest)
Product ID '101' Quantity in Inventories = 10
Product ID '101' Quantity Order_Items = 15
So 10-15=-5 (negative)
So...
Product ID '101' Quantity in Inventories = 10 (back to old data)
Delete '101' datas from Order_Items
Example #2:
Product ID '102' Quantity in Inventories = 20
Product ID '102' Quantity Order_Items = 8
So 20-8=12
So...
Product ID '102' Quantity in Inventories = 12
This is my code so far, any ideas? Thanks!
CREATE OR REPLACE TRIGGER update_qty
AFTER INSERT
ON order_items
FOR EACH ROW
DECLARE
qty_diff number;
qty_in number;
total number;
BEGIN
select quantity,count(product_id) into qty_in,total
from inventories
where product_id=:new.product_id;
if(total>1) then
select product_id from inventories where warehouse_id=(select min(warehouse_id) from inventories);
else
qty_diff:=qty_in-:new.quantity;
if(qty_diff<0) then
i.quantity:=:old.quantity;
delete from order_items where product_id=:new.product_id;
else
update inventories
set quantity=qty_diff;
end if;
end if;
END;
/
BEGIN
insert into order_items(order_id,item_id,product_id,quantity,unit_price) values(12345,12345,12345,100,200);
END;
/
It is better to apply this logic at the time of insertion(using procedure in DB or application-level logic). but if you are doing it for learning purposes and if I followed you correctly, You can use the following simple update in the trigger.
CREATE OR REPLACE TRIGGER UPDATE_QTY AFTER
INSERT ON ORDER_ITEMS
FOR EACH ROW
BEGIN
-- check and update the INVENTORIES table
-- lowest warehouse id with the product will be selected
-- new quantity or more must be available in the INVENTORIES table
UPDATE INVENTORIES I
SET
QUANTITY = QUANTITY - :NEW.QUANTITY
WHERE QUANTITY >= :NEW.QUANTITY
AND PRODUCT_ID = :NEW.PRODUCT_ID
AND NOT EXISTS (
SELECT 1
FROM INVENTORIES II
WHERE II.PRODUCT_ID = :NEW.PRODUCT_ID
AND II.WAREHOUSE_ID < I.WAREHOUSE_ID
);
-- if no record is selected means product is not available in any of the lowest warehouse
IF SQL%ROWCOUNT = 0 THEN
RAISE_APPLICATION_ERROR(
-20000,
'product not available in inventory'
);
END IF;
END;
/

Execute 2 loops for inserting values in SQL

I am using a procedure to get data from a table called Datastream which has 1000 rows and I have to read 100 records in a cursor once. And for each record match the primary key with foreign key in table Masterdata and move all the matching records in in multiple tables. Then the function should get the next 100 records from the table and do the same.
P.S: I need to use 2 loops as a condition.
I'm stuck with this error :
ORA-01722: invalid number
EDIT 1: Solved the above error, table datatype for 'product_id' was different then what it was supposed to be.
New Error: Everything seems fine, the procedure runs, but my tables aren't populated by the insert query. The output line prints SupID and PName.
I'm using the below code:
create or replace procedure newting is
s integer := 1;
e integer := s+99;
total_S integer := 1;
cursor endCount is
select datastream_id, ROW_NUMBER() OVER ( ORDER BY datastream_id )
from datastream
where datastream_id <= (select (count(*)/100) from datastream);
cursor transC is
SELECT datastream_id, product_id, customer_id, customer_name, outlet_id, outlet_name, quantity_sold, d_date
from datastream
where datastream_id between s and e
order by datastream_id;
TYPE val1 IS TABLE OF datastream.datastream_id%type;
v1 val1;
TYPE val2 IS TABLE OF datastream.product_id%type;
v2 val2;
TYPE val3 IS TABLE OF datastream.customer_id%type;
v3 val3;
TYPE val8 IS TABLE OF datastream.d_date%type;
v8 val8;
PName masterdata.product_name%TYPE;
SupID masterdata.supplier_id%TYPE;
SName masterdata.supplier_name%TYPE;
PPrice masterdata.sale_price%TYPE;
begin
open endCount;
open transC;
fetch transC bulk collect into v1,v2,v3,v4,v5,v6,v7,v8;
close endCount;
close transC;
for x in endCount
loop
for y in v1.first .. v1.last
loop
select product_name, supplier_id, supplier_name, sale_price into PName, SupID, SName, PPrice
from masterdata m
where m.product_id=v2(y);
Dbms_output.put_line(SupID);
insert into product (product_id, product_name) select v2(y), PName from dual --error in this line
where not exists (select * from product where product_id=v2(y));
insert into customer (customer_id, customer_name) select v3(y), v4(y) from dual
where not exists (select * from customer
where customer_id=v3(y));
insert into d_time (d_date, d_year, d_month, d_day)
select v8(y), to_char(v8(y),'YY'), to_char(v8(y),'MM'), to_char(v8(y),'DD')
from dual
where not exists (select * from d_time
where d_date=v8(y));
total_S := v7(y) * PPrice;
insert into sales_fact (transaction_id, product_id, supplier_id, outlet_id, customer_id, d_date, quantity, price, total_sales)
select v1(y), v2(y), SupID, v5(y), v3(y), v8(y), v7(y), PPrice, total_S
from dual
where not exists (select * from sales_fact
where transaction_id = v1(y));
end loop;
s:=s+99;
e:=e+99;
end loop;
end;
Any leads on this would be highly appreciated.
Thanks.
When you try to convert a character string into a number you could get An ORA-01722 error, basically the message says string cannot be converted into a number. Check all your DML(INSERT) specially the ones for dates. my advice is comment all (INSERT) but one, try to run it and if there is not issue, so proceed with the second one and keep going till you manage to find the problematic one and fix it(divide and conquer). Something else(just an observation), I believe the 'COMMIT' is out of this proc scope right?
Hope this could help.

The aggregate expression cannot be used in the WHERE clause

I have the following tables in my database:
The first table is named Amount, second Product, third Purchase.
And I should to create the trigger on insert to amount table. For example, I'll insert the following values: 4, 1, 10, where 4 is id_purchase, 1 is id_product and 4 is amount of this products. And trigger should subtract this amount from Amount_On_Stock. In my example, it should be: was 48, became 38.
Here's the code of my trigger:
CREATE TRIGGER AmountInsert ON Amount
AFTER INSERT
AS
BEGIN
UPDATE Product
SET Amount_On_Stock = (
SELECT
Amount_On_Stock
FROM Product
WHERE ID_Product = (
SELECT
MAX(ID_Product)
FROM Purchase
WHERE ID_Purchase = (
SELECT
MAX(ID_Purchase)
FROM Purchase
)
)
)-(
SELECT
Amount
FROM AMOUNT
WHERE ID_Product = (
SELECT
MAX(ID_Product)
FROM Purchase
WHERE ID_Purchase = (
SELECT
MAX(ID_Purchase)
FROM Purchase
)
)
)
END
But when I try to create this trigger I have the following error:
The aggregate expression cannot be used in the WHERE clause unless it
is contained in a subquery of the HAVING clause or in the select list,
and the column being aggregated is not an external reference.
So, how can I solve this problem?
Your trigger looks nothing like a SQL Server trigger. I would expect your trigger to look more like this:
CREATE TRIGGER AmountInsert ON Amount AFTER INSERT
AS
BEGIN
UPDATE p
SET Amount_On_Stock = p.Amount_On_Stock - i.amount
FROM Product p JOIN
inserted i
ON p.ID_Product = i.ID_Product;
END;
However, this will not do the right thing if you have multiple inserts on the same product at the same time. To handle that you need aggregation:
CREATE TRIGGER AmountInsert ON Amount AFTER INSERT
AS
BEGIN
UPDATE p
SET Amount_On_Stock = p.Amount_On_Stock - i.amount
FROM Product p JOIN
(SELECT i.ID_Product, SUM(i.amount) as amount
FROM inserted i
GROUP BY i.ID_Product
) i
ON p.ID_Product = i.ID_Product;
END;

SQLSTATE[42601]: Syntax error: 7 ERROR: subquery must return only one column Using Function

I get the error "subquery must return only one column" but i tried to use differents away to return the first record when i'm selecting the curProd.
I'm using this function, but i get the the errror as far as i know in:
curProd := (
SELECT "KeysForSale".*
FROM "KeysForSale"
WHERE row_STab.product_id = "KeysForSale".product_id AND (("KeysForSale".begin_date < payment_date AND "KeysForSale".end_date > payment_date) OR ("KeysForSale".discounted_price IS NULL))
ORDER BY "KeysForSale".discounted_price ASC NULLS LAST
LIMIT 1
);
The all function is:
CREATE FUNCTION "paymentRun"(buyer_id integer, payment_date DATE, payMethod paymentMethod, paid_amount double precision, payDetails text) RETURNS VOID AS
$$
DECLARE
row_STab "SearchTable"%rowtype;
curProd "KeysForSale"%rowtype;
totalPrice double precision;
returnedPID integer;
BEGIN
--For each entry in the search table
FOR row_STab IN
(
SELECT *
FROM "SearchTable"
)
LOOP
--We retrieve the associated product info, together with an available key
curProd := (
SELECT "KeysForSale".*
FROM "KeysForSale"
WHERE row_STab.product_id = "KeysForSale".product_id AND (("KeysForSale".begin_date < payment_date AND "KeysForSale".end_date > payment_date) OR ("KeysForSale".discounted_price IS NULL))
ORDER BY "KeysForSale".discounted_price ASC NULLS LAST
LIMIT 1
);
--Either there is no such product, or no keys for it
IF curProd IS NULL THEN
RAISE EXCEPTION 'Product is not available for purchase.';
END IF;
--Product's seller is the buyer - we can't let that pass
IF curProd.user_id = buyer_id THEN
RAISE EXCEPTION 'A Seller cannot purchase their own product.';
END IF;
--Fill in the rest of the data to prepare the purchase
UPDATE "SearchTable"
SET "SearchTable".price = (
CASE curProd.discounted_price IS NOT NULL -- if there was a discounted price, use it
WHEN TRUE THEN curProd.discounted_price
ELSE curProd.price
END
), "SearchTable".sk_id = curProd.sk_id
WHERE "SearchTable".product_id = curProd.product_id;
END LOOP;
--Get total cost
totalPrice := (
SELECT SUM("SearchTable".price)
FROM "SearchTable"
);
--The given price does not match the actual cost?
IF totalPrice <> paid_amount THEN
RAISE EXCEPTION 'Payment does not match cost!';
END IF;
--Create a purchase while keeping it's ID for register
INSERT INTO "Purchases" (purchase_id, final_price, user_id, paid_date, payment_method, details)
VALUES (DEFAULT, totalPrice, buyer_id, payment_date, payMethod, payDetails)
RETURNING purchase_id INTO returnedPID;
--For each product we wish to purchase
FOR row_STab IN
(
SELECT *
FROM "SearchTable"
)
LOOP
INSERT INTO "PurchasedKeys"(sk_id, purchase_id, price)
VALUES (row_STab.sk_id, returnedPID, row_STab.price);
UPDATE "SerialKeys"
SET "SerialKeys".user_id = buyer_id
WHERE row_STab.sk_id = "SerialKeys".sk_id;
END LOOP;
END
$$
LANGUAGE 'plpgsql' ;
Thank you in advance
Because the question has an incorrect answer, I'm providing an answer beyond the comment. The code that you want is:
curProd := (
SELECT "KeysForSale"
FROM "KeysForSale"
WHERE row_STab.product_id = "KeysForSale".product_id AND (("KeysForSale".begin_date < payment_date AND "KeysForSale".end_date > payment_date) OR ("KeysForSale".discounted_price IS NULL))
ORDER BY "KeysForSale".discounted_price ASC NULLS LAST
LIMIT 1
);
The difference is the lack of .*. Your version is returning a bunch of columns -- which is the error you are getting. You want to return a single record. The table name provides this.
I also think that parentheses will have the same effect:
SELECT ("KeysForSale".*)
For this case you should not to use syntax:
var := (SELECT ..).
Preferred should be SELECT INTO:
SELECT * INTO curProd FROM ...
The syntax SELECT tabname FROM tabname is PostgreSQL's proprietary, and although it is works well, better to not use, due unreadability for all without deeper PostgreSQL knowleadge.
Because PL/pgSQL is not case sensitive language, camel case is not advised (better to use snake case).
If it is possible, don't use ISAM style:
FOR _id IN
SELECT id FROM tab1
LOOP
SELECT * INTO r FROM tab2 WHERE tab2.id = _id
It is significantly slower than join (for more iterations)
FOR r IN
SELECT tab2.*
FROM tab1 JOIN tab2 ON tab1.id = tab2.id
LOOP
..
Cycles are bad for performance. This part is not really nice:
FOR row_STab IN
(
SELECT *
FROM "SearchTable"
)
LOOP
INSERT INTO "PurchasedKeys"(sk_id, purchase_id, price)
VALUES (row_STab.sk_id, returnedPID, row_STab.price);
UPDATE "SerialKeys"
SET "SerialKeys".user_id = buyer_id
WHERE row_STab.sk_id = "SerialKeys".sk_id;
END LOOP;
Possible solutions:
Use bulk commands instead:
INSERT INTO "PurchasedKeys"(sk_id, purchase_id, price)
SELECT sk_id, returnedPID, price
FROM "SearchTable"; -- using case sensitive identifiers is way to hell
UPDATE "SerialKeys"
SET "SerialKeys".user_id = buyer_id
FROM "SearchTable"
WHERE "SearchTable".sk_id = "SerialKeys".sk_id;
The less performance of ISAM style depends on number of iterations. For low iteration it is not important, for higher number it is death.

Searching for a row with a certain insert value, then comparing a value in that row with an insert value

I am trying to create an update trigger that checks the quantity of a product in stock over how much someone is ordering and displays a message if not enough of that product is in stock.
It's letting me create the trigger but when testing, it displays an error "Subquery returned more than 1 value. This is not permitted when the subquery follows =, !=,...."
I'm not really understanding how it's returning more than one value as I have it searching for the particular row with the product ID that matches the inserted value first. I then have it comparing the UnitsInStock from the inserted value.
Here's what I have so far:
CREATE TRIGGER tr_check_qty
ON OrderDetails
FOR UPDATE
AS
DECLARE #ProductID int,
#Quantity int
SELECT #ProductID = ProductID,
#Quantity = Quantity
FROM inserted
WHERE #ProductID = ( SELECT ProductID FROM Products )
IF
#Quantity > ( SELECT UnitsInStock FROM Products )
BEGIN
PRINT 'Not enough product in stock'
ROLLBACK TRANSACTION
END
I think you want something like:
CREATE TRIGGER tr_check_qty
ON OrderDetails
FOR UPDATE
AS
IF EXISTS (
SELECT *
FROM
Products p
inner join
inserted i
on p.ProductID = i.ProductID
WHERE i.Quantity > p.UnitsInStock)
BEGIN
PRINT 'Not enough product in stock'
ROLLBACK TRANSACTION
END
However, I'm a bit mystified on why this is inside an update trigger, as compared to an insert trigger.
SELECT UnitsInStock FROM Products as well as SELECT ProductID FROM Products may return a whole column, not a single value. You should specify some restriction there, like WHERE id = #someId.
You have more the one products right?
Then this line:
WHERE #ProductID = ( SELECT ProductID FROM Products )
And this line:
#Quantity > ( SELECT UnitsInStock FROM Products )
will return many rows.
Note as well that if you insert more then one row. The inserted table will have more then one row as well
You might want have to do something like this:
IF EXISTS
(
SELECT
NULL
FROM
inserted
WHERE EXISTS
(
SELECT
NULL
FROM
Product
WHERE
Product.Quantity>inserted.Quantity
)
)
BEGIN
PRINT 'Not enough product in stock'
ROLLBACK TRANSACTION
END