Oracle PL/SQL loop issue - loops

Hey guys trying to use a loop to cycle though days so my script doesn't fail the loop fails to execute each time I am not sure what I am doing wrong however i know for a fact the select statement is fine and working its just the loop i have trouble with also is it possible to have the loop check the max date in the table delete only the most recent day in case of incomplete data then add only whats needed to date?
Error line 41, column 4:
PL/SQL: ORA-00933: SQL command not properly ended
ORA-06550: line 7m Column 1:
Pl/sql statement ignored
delete Target_table
commit;
DECLARE
i_date date;
BEGIN
i_date := '01-Jan-2014';
WHILE i_date < sysdate LOOP
insert into Target_table
Select field_1,
field_2
From Data_table_1
LEFT JOIN Data_table_2
ON Data_table_1.ACCOUNT_ID=Data_table_2.account_id
Where Data_table_1.Date >= i_date
and Data_table_1.Date < i_date+1
and Data_table_1.COST_CENTRE In ('1','2','3','4','5','6','7','8','9','10')
And Data_table_1.field_3 In ('C', 'D')
And Data_table_1.field_4 Is Null
And Data_table_2.field_5 in (1,2)
END;
commit;
i_date := i_date +1;
END LOOP;
END

Just want to mention that the most efficent way is write the query in in SQL whenever possible, when PL/SQL can be avoided. (There is an overhead involved in the context switch between SQL and PL/SQL blocks). This is how i would write the query. The new block, using CONNECT by would generate all dates between i_date and sysdate
DECLARE
i_date DATE;
BEGIN
DELETE Target_table;
SELECT field_1,
field_2
FROM Data_table_1
JOIN (SELECT i_date + level -1 as i_date_new
FROM DUAL
CONNECT BY LEVEL<=TRUNC(sysdate)-i_date
)dates_generated
ON Data_table_1.Date_field1 >= dates_generated.i_date_new
AND Data_table_1.Date_field1 < dates_generated.i_date_new+1
LEFT JOIN Data_table_2
ON Data_table_1.ACCOUNT_ID =Data_table_2.account_id
WHERE Data_table_1.COST_CENTRE IN ('1','2','3','4','5','6','7','8','9','10')
AND Data_table_1.field_3 IN ('C', 'D')
AND Data_table_1.field_4 IS NULL
AND Data_table_2.field_5 IN (1,2);
END;

I have made some modifications in your code. I think this should work. I have also mentioned comments for which i have made changes.
DECLARE
i_date DATE;
BEGIN
DELETE Target_table; -- Inserted inside the Anonymous block
i_date := to_date('10-Nov-2015','DD-MON-YYYY'); -- Specified Date format
WHILE i_date < sysdate
LOOP
INSERT INTO Target_table
SELECT field_1,
field_2
FROM Data_table_1
LEFT JOIN Data_table_2
ON Data_table_1.ACCOUNT_ID =Data_table_2.account_id
WHERE Data_table_1.Date_field1 >= i_date
AND Data_table_1.Date_field1 < i_date+1
AND Data_table_1.COST_CENTRE IN ('1','2','3','4','5','6','7','8','9','10')
AND Data_table_1.field_3 IN ('C', 'D')
AND Data_table_1.field_4 IS NULL
AND Data_table_2.field_5 IN (1,2);
dbms_output.put_line(i_date);
i_date := i_date +1;
END LOOP;
COMMIT; -- Added commit at the end
END;

Related

Trigger is not working properly in SQL Server

I understand that perhaps the problem is that I use a select on the same table that I update or insert a record, but this trigger throws an exception in most cases. Then what should I rewrite?
The purpose of the trigger is to block inserting or updating entries if the room is already occupied on a certain date, i.e. the dates overlap
CREATE TABLE [dbo].[settlements]
(
[id] [int] IDENTITY(1,1) NOT NULL,
[client_id] [int] NOT NULL,
[checkin_date] [date] NOT NULL,
[checkout_date] [date] NOT NULL,
[room_id] [int] NOT NULL,
[employee_id] [int] NULL
);
ALTER TRIGGER [dbo].[On_Hotel_Settlement]
ON [dbo].[settlements]
AFTER INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON;
DECLARE #room_id int
DECLARE #checkin_date Date, #checkout_date Date
DECLARE cursor_settlement CURSOR FOR
SELECT room_id, checkin_date, checkout_date
FROM inserted;
OPEN cursor_settlement;
FETCH NEXT FROM cursor_settlement INTO #room_id, #checkin_date, #checkout_date;
WHILE ##FETCH_STATUS = 0
BEGIN
IF EXISTS (SELECT 1
FROM settlements AS s
WHERE s.room_id = #room_id
AND ((s.checkin_date >= #checkin_date AND s.checkin_date <= #checkout_date)
OR (s.checkout_date >= #checkin_date AND s.checkout_date <= #checkout_date)))
BEGIN
RAISERROR ('Room is not free', 16, 1);
ROLLBACK;
END;
FETCH NEXT FROM cursor_settlement INTO #room_id, #checkin_date, #checkout_date;
END;
CLOSE cursor_settlement;
DEALLOCATE cursor_settlement;
RETURN
I tried to test the code by removing the condition with dates and leaving only the room _id, but the trigger does not work correctly in this case either.
I tried query like
IF EXISTS (SELECT 1
FROM settlements AS s
WHERE s.room_id = 9
AND ((s.checkin_date >= '2022-12-10' AND s.checkin_date <= '2022-12-30')
OR (s.checkout_date >= '2022-12-10' AND s.checkout_date <= '2022-12-30')))
BEGIN
RAISERROR ('Room is not free', 16, 1);
END;
and it worked correctly. Problem is that is not working in my trigger
As noted by comments on the original post above, the cursor loop is not needed and would best be eliminated to improve efficiency.
As for the date logic, consider a new record with a check-in date that is the same as the checkout date from a prior record. I believe that your logic will consider this an overlap and throw an error.
My suggestion is that you treat the check-in date as inclusive (that night is in use) and the checkout date as exclusive (that night is not in use).
A standard test for overlapping dates would then be Checkin1 < Checkout2 AND Checkin2 < Checkout1. (Note use of inequality.) It may not be obvious, but this test covers all overlapping date cases. (It might be more obvious if this condition is inverted and rewritten as NOT (Checkin1 >= Checkout2 OR Checkin2 >= Checkout1).)
Also, if you are inserting multiple records at once, I would suggest that you also check the inserted records for mutual conflicts.
Suggest something like:
ALTER TRIGGER [dbo].[On_Hotel_Settlement]
ON [dbo].[settlements]
AFTER INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON;
IF EXISTS(
SELECT *
FROM inserted i
JOIN settlements s
ON s.room_id = i.room_id
AND s.checkin_date < i.checkout_date
AND i.checkin_date < s.checkout_date
AND s.id <> i.id
)
BEGIN
RAISERROR ('Room is not free', 16, 1);
ROLLBACK;
END;
RETURN;
END
One more note: Be careful with an early rollback of a transaction. If your overall logic could potentially execute additional DML after the error is thrown, that would now execute outside the transaction and there would be no remaining transaction to roll back.

Stored procedure inserting the same record repeatedly instead of looping through list from SELECT

I am fairly new at writing procedures (beyond the basics)
I am trying to write a stored procedure that inserts into a table (dbo.billing_batch) based on a select statement that loops through the list of results (#DealerID FROM dbo.vehicle_info).
The SELECT DISTINCT... statement on its own works perfectly and returns a list of 54 records.
The result of the SELECT statement is dynamic and will change from week to week, so I cannot count on 54 records each time.
I am trying to use WHILE #DealerID IS NOT NULL to loop through the INSERT routine.
The loop is supposed to update dbo.billing_batch, however it is inserting the same 1st record (BillingBatchRosterID, DealerID) over and over and over to infinity.
I know I must be doing something wrong (I have never written a stored procedure that loops).
Any help would be greatly appreciated!
Here is the stored procedure code:
ALTER PROCEDURE [dbo].[sp_billing_batch_set]
#varBillingBatchRosterID int
AS
SET NOCOUNT ON;
BEGIN
DECLARE #DealerID int
SELECT DISTINCT #DealerID = vi.DealerID
FROM dbo.vehicle_info vi
LEFT JOIN dbo.dealer_info di ON di.DealerID = vi.DealerID
WHERE di.DealerActive = 1
AND (vi.ItemStatusID < 4 OR vi.ItemStatusID = 5 OR vi.ItemStatusID = 8)
END
WHILE #DealerID IS NOT NULL
BEGIN TRY
INSERT INTO dbo.billing_batch (BillingBatchRosterID, DealerID)
VALUES(#varBillingBatchRosterID, -- BillingBatchRosterID - int
#DealerID) -- DealerID - int
END TRY
BEGIN CATCH
SELECT ' There was an error: ' + error_message() AS ErrorDescription
END CATCH
You have the same problems as another recent post here: Iterate over a table with a non-int id value
Why do a loop? Just do it as a single SQL statement
If you must use a loop, you will need to update your #Dealer value at each run (e.g., to the next DealerId) otherwise it will just infinitely loop with the same DealerID value
Don't do a loop.
Here's an example not needing a loop.
ALTER PROCEDURE [dbo].[P_billing_batch_set]
#varBillingBatchRosterID int
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRY
INSERT INTO dbo.billing_batch (DealerID, BillingBatchRosterID)
SELECT DISTINCT vi.DealerID, #varBillingBatchRosterID
FROM dbo.vehicle_info vi
INNER JOIN dbo.dealer_info di ON di.DealerID = vi.DealerID
WHERE di.DealerActive = 1
AND (vi.ItemStatusID < 4
OR vi.ItemStatusID = 5
OR vi.ItemStatusID = 8
);
END TRY
BEGIN CATCH
SELECT ' There was an error: ' + error_message() AS ErrorDescription;
END CATCH;
END;
Note I
Changed the LEFT JOIN to an INNER JOIN as your WHERE clause needs the record to exist in the dealer_info table
Moved the SET NOCOUNT ON; to be within the BEGIN-END section
Moved the END to the end
Renamed your stored procedure as per the excellent comment from #marc_s (on the question itself)

Use if/else statement without else branch

I don't know how to use if else in this case. When score > 10, stop insert. Else continue insert as normally. But what is the syntax to do that?
CREATE TRIGGER invalidScore ON dbo.dbo_score
AFTER INSERT
AS
DECLARE #score DECIMAL;
SET #score = (SELECT s.score FROM Inserted s);
IF(#score > 10)
BEGIN
RETURN 'score must be less than 10'
ROLLBACK TRAN
END
ELSE
BEGIN
END
There are 3 things you need to change for this trigger to work:
Remove the else section - its optional.
Handle the fact that Inserted may have multiple rows.
Throw the error rather than using the return statement so you can handle it in the client. And throw it after rolling back the transaction in progress.
Corrected trigger follows:
create trigger invalidScore on dbo.dbo_score
after insert
as
begin
if exists (select 1 from Inserted S where S.Score > 10) begin
rollback tran;
throw 51000, 'score must be less than 10', 1;
end
end
First, creating these types of sql objects should use begin.. end blocks. Second is,you can ignore the else statement.
CREATE TRIGGER invalidScore ON dbo.dbo_score
AFTER INSERT
AS
BEGIN
DECLARE #score DECIMAL;
SET #score = (SELECT s.score FROM Inserted s);
IF(#score > 10)
BEGIN
RETURN 'score must be less than 10'
ROLLBACK TRAN
END
END
'Else' is an option section you can remove this and use it,but i may like you to consider using check constraints for scenarios like this rather than adding a trigger check on score column
e.g.
CREATE TABLE dbo.dbo_score(
Score int CHECK (score < 10)
);
A CHECK constraint is faster, simpler, more portable, needs less code and is less error prone

SQL Server: the ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION

I have a trigger that works (it fires when it has to) but I still get an error.
I understand the error but I don't know how to resolve it.
I tried to put some BEGIN TRANSACTION with all the code who go with it but I think my grammar is wrong because I always get a timeout!
So my question is, where exactly do I have to put my BEGIN TRANSACTION statements in my code?
Also, do I need 3 BEGIN TRANSACTION statements since I have 3 ROLLBACK?
Thank you in advance!
My code:
ALTER TRIGGER [dbo].[Tr_CheckOverlap]
ON [dbo].[Tranche]
FOR INSERT
AS
BEGIN
SET NOCOUNT ON;
DECLARE #IdVol INT, #IdTranche INT,
#AgeMinInserted DATE, #AgeMaxInserted DATE
SELECT #AgeMinInserted = t.TRA_Age_Min
FROM Tranche t
JOIN inserted AS i ON t.TRA_Id = i.TRA_Id
SELECT #AgeMaxInserted = t.TRA_Age_Max
FROM Tranche t
JOIN inserted AS i ON t.TRA_Id = i.TRA_Id
DECLARE CR_TrancheVol CURSOR FOR
SELECT t.TRA_Vol_Id,t.TRA_Id
FROM Tranche t
JOIN inserted AS i ON t.TRA_Vol_Id = i.TRA_Vol_Id;
OPEN CR_TrancheVol
FETCH CR_TrancheVol INTO #IdVol, #IdTranche
WHILE( ##FETCH_STATUS = 0)
BEGIN
DECLARE #AgeMin DATE, #AgeMax DATE
SELECT #AgeMin = t.TRA_Age_Min
FROM Tranche t
WHERE t.TRA_Id = #IdTranche
SELECT #AgeMax = t.TRA_Age_Max
FROM Tranche t
WHERE t.TRA_Id = #IdTranche
IF #AgeMinInserted > #AgeMin AND #AgeMinInserted < #AgeMax
BEGIN
PRINT 'Trans1'
RAISERROR('Overlap: Date de naissance minimum déjà couverte', 1, 420)
ROLLBACK TRANSACTION
END
IF #AgeMaxInserted > #AgeMin AND #AgeMaxInserted < #AgeMax
BEGIN
PRINT 'Trans2'
RAISERROR('Overlap: Date de naissance maximum déjà couverte', 1, 421)
ROLLBACK TRANSACTION
END
IF #AgeMinInserted < #AgeMin AND #AgeMaxInserted > #AgeMax
BEGIN
PRINT 'Trans3'
RAISERROR('Overlap: Tranche déjà couverte complètement', 1, 422)
ROLLBACK TRANSACTION
END
FETCH CR_TrancheVol INTO #IdVol, #IdTranche
END
CLOSE CR_TrancheVol
DEALLOCATE CR_TrancheVol
END
EDIT:
Okay, so I tried your answer without cursor (I understand that my way was clearly not the best!) but for now it doesn't work.
My goal: I have a DB to book a flight. In this DB, i have a table "Tranche" who contains some dates and some prices (depending when the flight is).
I need to prevent and avoid any overlap of birthdate, for example:
1y-17y: 80€
18y-64y: 120€
So my trigger has to fire when I try to insert 17y-63y: xx € (because I already have a price for those ages).
Sorry if my English is not perfect btw!
Here's my table "Tranche":
https://i.stack.imgur.com/KuQH8.png
TRA_Vol_ID is a foreign key of another table "Vol" who contain the flights
Here's the code I have atm:
ALTER TRIGGER [dbo].[Tr_CheckOverlap]
ON [dbo].[Tranche]
FOR INSERT
AS
BEGIN
/*
Some SQL goes here to get the value of Minimum age.
I assuming that it doesn't vary by entry, however,
I don't really have enough information to go on to tell
*/
SET NOCOUNT ON;
DECLARE #MinAge DATE, #MaxAge DATE
SELECT #MinAge = t.TRA_Age_Min
FROM Tranche t
JOIN Vol AS v ON v.VOL_Id = t.TRA_Vol_Id
JOIN inserted AS i ON t.TRA_Id = i.TRA_Id
WHERE t.TRA_Id = i.TRA_Id
SELECT #MaxAge = t.TRA_Age_Max
FROM Tranche t
JOIN inserted AS i ON t.TRA_Id = i.TRA_Id
JOIN Vol AS v ON v.VOL_Id = t.TRA_Vol_Id
WHERE t.TRA_Id = i.TRA_Id
IF (SELECT COUNT(CASE WHEN i.TRA_Age_Min > #MinAge AND i.TRA_Age_Min < #MaxAge THEN 1 END) FROM inserted i) > 0
BEGIN
RAISERROR('Overlap: Birthday min reached',1,430);
ROLLBACK
END
ELSE IF (SELECT COUNT(CASE WHEN i.TRA_Age_Max > #MinAge AND i.TRA_Age_Max < #MaxAge THEN 1 END) FROM inserted i) > 0
BEGIN
RAISERROR('Overlap: Birthday max reached',1,430);
ROLLBACK
END
END
I don't really know what the OP's goals are here. However, I wanted to post a small example how to do a dataset approach, and how to check all the rows in one go.
At the moment, the trigger the OP has will only "work" if the user is inserting 1 row. Any more, and things aren't going to work properly. Then we also have the problem of the CURSOR. I note that the declaration of the cursors aren't referencing inserted at all, so I don't actually know what their goals are. It seems more like the OP is auditing the data already in the table when a INSERT occurs, not the data that is being inserted. This seems very odd.
Anyway, this isn't a solution for the OP, however, I don't have enough room in a comment to put all this. Maybe it'll push the OP in the right direction.
ALTER TRIGGER [dbo].[Tr_CheckOverlap]
ON [dbo].[Tranche]
FOR INSERT
AS
BEGIN
/*
Some SQL goes here to get the value of Minimum age.
I assuming that it doesn't vary by entry, however,
I don't really have enough information to go on to tell
*/
IF (SELECT COUNT(CASE WHEN i.Age < #MinAge THEN 1 END) FROM inserted i) > 0 BEGIN
RAISERROR('Age too low',1,430);
ROLLBACK
END
ELSE
IF (SELECT COUNT(CASE WHEN i.Age > #MaxAge THEN 1 END) FROM inserted i) > 0 BEGIN
RAISERROR('Age too high',1,430);
ROLLBACK
END
END
The question at hand seems to very much be an xy question; the problem isn't the CURSOR or the ROLLBACK, the problems with this trigger are much more fundamental. I'd suggest revising your question and actually explaining your goal of what you want to do with your Trigger. Provide DDL to CREATE your table and INSERT statements for any sample data. You might want to also provide some INSERT statements that will have different results for your trigger (make sure to include ones that have more than one row to be inserted at a time).
I realise this is more commenting, however, again, there is definitely not enough room in a comment for me to write all this. :)

Atomic Transaction with nested BEFORE INSERT/UPDATE Triggers

Currently i am implementing a procedure, which creates a couple of rows in some related tables out of a template. So my Procedure consists of a SAVEPOINT followed by some INSERT statements on different tables, and a Cursor to insert some more rows to other tables while referencing on the newly created primary keys.
Each of those tables has an BEFORE INSERT/UPDATE trigger defined which has the purpose to:
Get a new primary key from a sequencer if it is not defined in the INSERT statement (there are cases where I need to set the Primary key explicitely to reference it later on in the same transaction)
Set some default values if they are NULL
Set auditing fields (last_change_date, last_change_user, etc..)
The transaction fails with ORA-04091: table is mutating, trigger/function may not see it
I am understanding, that I could Workaround this, by declaring PRAGMA AUTONOMOUS TRANSACTION in each Trigger, but my Transaction would not be atomic any more then, as it is the requirement that all those datasets should be created/inserted as a whole or None of them.
So what am I doing wrong in the design of my database?
UPDATE: This is the Code of the trigger
CREATE TRIGGER TRG_AUFTRAG_B_IU
BEFORE INSERT OR UPDATE
ON AUFTRAG
FOR EACH ROW
BEGIN
IF INSERTING THEN
IF :new.id is NULL or :new.id = 0 THEN
SELECT SEQ_AUFTRAG.nextval into :new.id from dual;
END IF;
IF :new.nummer is NULL or :new.nummer = 0 THEN
SELECT nvl(MAX(NUMMER),0)+1 INTO :new.nummer FROM AUFTRAG WHERE EXTRACT(YEAR from DATUM) = EXTRACT(YEAR from :new.DATUM);
END IF;
--DEFAULT Values
IF :new.BETR_GRENZWERTE_RELEVANT is NULL THEN
SELECT 0 INTO :new.BETR_GRENZWERTE_RELEVANT FROM dual;
END IF;
IF :new.DOKUMENTE_ABGELEGT is NULL THEN
SELECT 0 INTO :new.DOKUMENTE_ABGELEGT FROM dual;
END IF;
IF :new.EXT_ORG is NULL or :new.EXT_ORG < 1 THEN
SELECT 1 INTO :new.EXT_ORG FROM dual;
END IF;
:new.ERSTELLT_VON := nvl(:new.ERSTELLT_VON,user);
:new.ERSTELLT_DATUM := nvl(:new.ERSTELLT_DATUM,sysdate);
END IF;
:new.GEAENDERT_VON := user;
:new.GEAENDERT_DATUM := sysdate;
END;
You can write it more compact like this:
CREATE TRIGGER TRG_AUFTRAG_B_IU
BEFORE INSERT OR UPDATE
ON AUFTRAG
FOR EACH ROW
BEGIN
IF INSERTING THEN
:new.id = NVL(NULLIF(:new.id, 0), SEQ_AUFTRAG.nextval);
--DEFAULT Values
:new.BETR_GRENZWERTE_RELEVANT := NVL(:new.BETR_GRENZWERTE_RELEVANT, 0);
:new.DOKUMENTE_ABGELEGT := NVL(:new.DOKUMENTE_ABGELEGT, 0);
IF :new.EXT_ORG is NULL or :new.EXT_ORG < 1 THEN
:new.EXT_ORG := 1;
END IF;
:new.ERSTELLT_VON := nvl(:new.ERSTELLT_VON,user);
:new.ERSTELLT_DATUM := nvl(:new.ERSTELLT_DATUM,sysdate);
END IF;
:new.GEAENDERT_VON := user;
:new.GEAENDERT_DATUM := sysdate;
END;
Only "problem" is this part
IF :new.nummer is NULL or :new.nummer = 0 THEN
SELECT nvl(MAX(NUMMER),0)+1 INTO :new.nummer
FROM AUFTRAG
WHERE EXTRACT(YEAR from DATUM) = EXTRACT(YEAR from :new.DATUM);
END IF;
This one you should put into your procedure or in a statement trigger (i.e. without FOR EACH ROW clause) like this:
CREATE TRIGGER TRG_AUFTRAG_B_A
AFTER INSERT ON AUFTRAG
BEGIN
UPDATE
(SELECT ID, NUMMER,
ROW_NUMBER() OVER (PARTITION BY EXTRACT(YEAR from DATUM) ORDER BY ID) as N
FROM AUFTRAG)
SET NUMMER = N
WHERE NUMMER IS NULL;
END;

Resources