How to handle plsql errors and exceptions - plsql

My code structure is as below.
PKG1
Procedure TEST1()
BEGIN
PKG2.TEST2;
PKG_X.TEST_X
EXCEPTION HANDLING
END;
PKG2
Procedure TEST2
BEGIN
If(Condition is met)THEN
//Should raise an error message
END IF;
END;
A procedure(TEST1) in PKG1 calls a procedure(TEST2) in PKG2. There is an exception handling part in PKG1 but not in PKG2.
In TEST2, I want to raise an Error and stop the flow when a certain condition is met. When I debug the code it was noticed that the error message does not stop the flow. 'PKG_X.TEST_X' line is also executed. The error message can be seen in the Exception handling part and the debug code of TEST1 part. What would be the possible way to stop the flow while calling 'PKG2.TEST2'. Assume that the syntax and the functionality is correct.

You didn't post the important part: how exactly you raise an error? If you want to stop execution, then
Procedure TEST2
BEGIN
If(Condition is met)THEN
--Should raise an error message
raise_application_error(-20000, 'Stop execution'); --> this
END IF;
END;

Related

UI Dialogue popping up in automated testing

As a total newbie to AL, I'm trying to set up some automated tests for an upcoming upgrade to BC19. As a proof-of-concept I'm still working in the Cronus sample database, but it's giving me a problem. I'm trying to test the Item Card; I want to create a new item, give it an unacceptable value for a couple of fields, and have it error at me.
The test seems to work just fine, except for one element - whenever I run it, I get a Confirm dialogue asking if I want to rename the record. Regardless of what I select, the test completes successfully; it doesn't report an Unhandled UI error.
I've tried adding a ConfirmHandler and a MessageHandler; I've already got a ModalPageHandler that works great. When I add the ConfirmHandler or MessageHandler though, it errors out, telling me that the handlers weren't called.
Here's my code:
[Test]
[HandlerFunctions('HandleConfigTemplates')]
Procedure AddBadTypeItem()
var
pgeItem: TestPage "Item Card";
begin
pgeItem.OpenNew();
pgeItem."No.".SetValue('zzzzz');
pgeItem."Description".SetValue('zzzzz');
Asserterror pgeItem."Type".SetValue('zzzz');
end;
[ModalPageHandler]
Procedure HandleConfigTemplates(
var ConfigTemplates: TestPage "Config Templates")
begin
ConfigTemplates.OK.Invoke();
end;
[ConfirmHandler]
Procedure HandleConfirmNo(Question: Text[1024]; var Reply: Boolean)
begin
Reply := False;
end;
Here's the dialogue I see:
And the error I see if I try to include the handlers:
The following UI handlers were not executed: HandleConfirmDialogue,MessageHandler

What is the difference between Do and Do on error in Progress 4gl?

I have observed that in both cases, statements under these two blocks will execute the same. I do not understand what the difference is. Please can you explain.
Not surprisingly ON ERROR has to do with error handling. You should read up on this in the online help/manual since there's lots of ways on what to do.
DO is basically just a block. Without anything else it really doesn't do a lot. Paired with statements like TRANSACTION or ON ERROR it can greatly change how your program executes. You should check out the NO-ERROR statement as well. It also effects error handling.
In the below examples I force an error by trying to cast the string HELLO to an integer, this doesn't work of course.
DO ON ERROR, RETRY
This will repeat the block if there's an error and setting RETRY to true. If you don't LEAVE in the RETRY-block you will have a loop.
DO ON ERROR UNDO, RETRY:
IF RETRY THEN DO:
DISPLAY "RETRY".
/* Do some cleanup or what else */
LEAVE.
END.
i = INTEGER("HELLO").
END.
DO ON ERROR, THROW
A perhaps more modern approach when THROW - CATCH is used. Note that this also supresses the error from appearing (a bit like NO-ERROR).
DEFINE VARIABLE i AS INTEGER NO-UNDO.
DO ON ERROR UNDO, THROW:
i = INTEGER("HELLO").
END.
CATCH error AS Progress.Lang.Error :
MESSAGE "We had an error".
END CATCH.
DO:
The program will just halt on error
DEFINE VARIABLE i AS INTEGER NO-UNDO.
DO:
i = INTEGER("HELLO").
END.
The ON ERROR statement gives you control on what happens when the block fails. If you are using ROUTINE-LEVEL error handling for example, errors at the block level are not caught by default, so you can
DO ON ERROR UNDO,THROW:
END.
This will make sure the error is trapped. If you are using BLOCK-LEVEL error handling then this would be trapped by default.
This is just an example, and there are many things you can use ON ERROR for. Have a look at this documentation: https://help.consultingwerkcloud.com/openedge/117/rfi1424919692411.html

Catching errors from procedures outside of processing block

I have following code (it's simplified for illustration purposes). I'm creating records in different DB tables in proc1, proc2, and proc3. What I'm trying to achieve is...if I encounter an error while looping through temp-tables at any point (even after I created a bunch of DB records already), I want to roll everything back so no records are created. It catches errors if proc1, proc2, and proc3 with no issues but I cannot figure out how to pass those errors to the main processing block so it understands it and rolls everything back. In other words, the message ('error # main trans block') never pops up so the already created records stay in the DB. As a matter of fact, nothing gets rolled back.
DO TRANSACTION ON ERROR UNDO, THROW:
FOR EACH tt1:
RUN proc1.
FOR EACH tt2 WHERE tt2.field1 EQ tt1.field1:
RUN proc2.
FOR EACH tt3 WHERE tt3.field2 EQ tt2.field2:
RUN proc3.
END.
END.
END.
CATCH e AS PROGRESS.Lang.AppERROR:
MESSAGE 'error # main trans block'
VIEW-AS ALERT-BOX INFO BUTTONS OK.
END CATCH.
END.
PROCEDURE proc1.
DO TRANSACTION ON ERROR UNDO, THROW:
/* creating some DB records */
CATCH e AS PROGRESS.Lang.ERROR:
RETURN ERROR 'Proc1 ' + e:getmessage(1).
END CATCH.
END.
END PROCEDURE.
PROCEDURE proc2.
DO TRANSACTION ON ERROR UNDO, THROW:
/* creating some DB records */
CATCH e AS PROGRESS.Lang.ERROR:
RETURN ERROR 'Proc2 ' + e:getmessage(1).
END CATCH.
END.
END PROCEDURE.
PROCEDURE proc3.
DO TRANSACTION ON ERROR UNDO, THROW:
/* creating some DB records */
CATCH e AS PROGRESS.Lang.ERROR:
RETURN ERROR 'Proc3 ' + e:getmessage(1).
END CATCH.
END.
END PROCEDURE.
TIA
There are a couple of potential issues.
First, your temp-tables tt1 and tt2 need to be defined without the NO-UNDO flag.
Second, the FOR EACH blocks are using their default error handling behavior, which is ON ERROR UNDO, NEXT. So errors raised within the FOR EACH blocks will cause the current iteration to be undone, not the whole transaction.
I recommend adding the
BLOCK-LEVEL ON ERROR UNDO, THROW .
to the top of the program. Or at least
ROUTINE-LEVEL ON ERROR UNDO, THROW .
in combination with an ON ERROR UNDO, THROW option on all the FOR EACH blocks.
The BLOCK-LEVEL error handling option is available since OpenEdge 11.3 (or so).

simple way to output messages from a stored procedure in Teradata

Using SQL*Assistant:
REPLACE PROCEDURE test_proc()
BEGIN
DECLARE l_msg varchar(128);
set l_msg = 'test';
-- PRINT is not supported
--print l_msg;
-- debug is recognized as a special token, but doesn't work
--debug l_msg;
-- this does nothing
--SIGNAL SQLSTATE '02000';
END;
Is there a simple way to output a text during a procedure execution, aside writing to a log table?
TD 14.xx
EDIT:
Not trying to handle exceptions, but rather send text messages to the client, as the procedure progresses, regardless of the state/condition, similar to PRINT (Sybase), DMBS_OUTPUT(Oracle), DEBUG(SQL Server).

How to ROLLBACK a transaction when testing using tSQLt

I recently was calling a procedure that contained a rasierror in the code. The raiserror was in a try catch block. Also a BEGIN TRAN was in the same try catch block after the raiserror. The Catch block is designed to ROLLBACK the transaction if the error occurred in the transaction. The way it does this is to check the ##TRANCOUNT if it is greater that 0 I know that it had started a transaction and needs to ROLLBACK. When testing with tSQLt the ##TRANCOUNT is always >0 so if it ever hits the CATCH Block the ROLLBACK is executed and tSQLt fails (because tSQLt is running in a transaction). When I rasie an error and the CATCH block is run tSQLt always fails the test. I have no way to test for the correct handling of the raiserror. How would you create a test case that can potentially ROLLBACK a transaction?
As you mentioned, tSQLt runs every test in its own transaction. To keep track of what is going on is relies on that same transaction to be still open when the test finishes. SQL Server does not support nested transactions, so your procedure rolls back everything, including the status information the framework stored for the current test. At that point tSQLt can only assume that something really bad happened. It therefore marks the test as errored.
SQL Server itself discourages a rollback inside a procedure, by throwing an error if that procedure was called within an open transaction. For ways to deal with this situation and some additional info check out my blog post about how to rollback in procedures.
As I'm just reading up on tSQLt this was one of the first questions that came to mind when I've learned each test ran in a transactions. As some of my stored procedures do start transaction, some even use nested transactions, this can become challenging. What I've learned about nested transactions, if you apply the following rules you can keep your code clean of constant error checking and still handle errors gracefully.
Always use a TRY/CATCH block when opening a transactions
Always commit the transactions unless an error was raised
Always rollback the transaction when an error is raised unless ##TRANCOUNT = 0
Always reraise the error unless you're absolutely sure there was no transaction open at the start of the stored procedure.
Keeping those rules in mind here is an example of a proc implementation and test code to test it.
ALTER PROC testProc
#IshouldFail BIT
AS
BEGIN TRY
BEGIN TRAN
IF #IshouldFail = 1
RAISERROR('failure', 16, 1);
COMMIT
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK;
-- Do some exception handling
-- You'll need to reraise the error to prevent exceptions about inconsistent
-- ##TRANCOUNT before / after execution of the stored proc.
RAISERROR('failure', 16, 1);
END CATCH
GO
--EXEC tSQLt.NewTestClass 'tSQLt.experiments';
--GO
ALTER PROCEDURE [tSQLt.experiments].[test testProc nested transaction fails]
AS
BEGIN
--Assemble
DECLARE #CatchWasHit CHAR(1) = 'N';
--Act
BEGIN TRY
EXEC dbo.testProc 1
END TRY
BEGIN CATCH
IF ##TRANCOUNT = 0
BEGIN TRAN --reopen an transaction
SET #CatchWasHit = 'Y';
END CATCH
--Assert
EXEC tSQLt.AssertEqualsString #Expected = N'Y', #Actual = #CatchWasHit, #Message = N'Exception was expected'
END;
GO
ALTER PROCEDURE [tSQLt.experiments].[test testProc nested transaction succeeds]
AS
BEGIN
--Act
EXEC dbo.testProc 0
END;
GO
EXEC tSQLt.Run #TestName = N'tSQLt.experiments'
Better to use a BEGIN TRY block after BEGIN TRANSACTION. I did this when I had a similar problem. This is more logical, because in CATCH block I checked IF ##TRANCOUNT > 0 ROLLBACK. This condition doesn't need to be checked if another error is raised before BEGIN TRANSACTION. And in this case you can test your RAISERROR functionality.
+1 to both the above answers.
However, if you don't want to use TRY .. CATCH, please try the following code. The part between the lines ----- represents the test, and above and below that represents tSQLt, before and after it calls your test. As you can see, the transaction started by tSQLt before calling the test, is still in place, as it expects, whether or not the error occurs. ##TRANSCOUNT is still 1
You can comment out the RAISERROR to try it with and without the exception being raised.
SET NOCOUNT ON
BEGIN TRANSACTION -- DONE BY tSQLt
PRINT 'Inside tSQLt before calling the test: ##TRANCOUNT = ' + CONVERT (VARCHAR, ##TRANCOUNT)
---------------------------------
PRINT ' Start of test ---------------------------'
SAVE TRANSACTION Savepoint
PRINT ' Inside the test: ##TRANCOUNT = ' + CONVERT (VARCHAR, ##TRANCOUNT)
BEGIN TRANSACTION -- PART OF THE TEST
PRINT ' Transaction in the test: ##TRANCOUNT = ' + CONVERT (VARCHAR, ##TRANCOUNT)
RAISERROR ('A very nice error', 16, 0)
PRINT ' ##ERROR = ' + CONVERT(VARCHAR,##ERROR)
-- PART OF THE TEST - CLEAN-UP
IF ##ERROR <> 0 ROLLBACK TRANSACTION Savepoint -- Not all the way, just tothe save point
ELSE COMMIT TRANSACTION
PRINT ' About to finish the test: ##TRANCOUNT = ' + CONVERT (VARCHAR, ##TRANCOUNT)
PRINT ' End of test ---------------------------'
---------------------------------
ROLLBACK TRANSACTION -- DONE BY tSQLt
PRINT 'Inside tSQLt after finishing the test: ##TRANCOUNT = ' + CONVERT (VARCHAR, ##TRANCOUNT)
With acknowledgement to information and code at http://www.blackwasp.co.uk/SQLSavepoints.aspx

Resources