Nested stored procedure exception handling - stored-procedures

I have one problem.. in every stored procedure i have
BEGIN TRY
Command and every exception i log into error table.
But now i need execute stored procedure inside another stored procedure and i realized that nested exception is populated into parent stored procedure and after that is logged.
This is my CATCH construction inside every stored procedure
BEGIN
BEGIN TRY
BEGIN TRANSACTION;
<CODE>
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION;
...
INSERT INTO ErrorLog (ObjectName...)
VALUES (OBJECT_SCHEMA_NAME(##PROCID) + '.' + OBJECT_NAME(##PROCID)...)
RAISERROR(#ErrorMessage, #ErrorSeverity, #ErrorState);
END CATCH
Example scenario could be
SP1 => Fires SP2 => Fires SP3 => Fires SP4 => Fires SP5
If error happened in SP4 into Error table are inserted rows
SP1 : Error message from SP4
SP2 : Error message from SP4
SP3 : Error message from SP4
So the problem is that i need
SP4 : Error message from SP4
and if it is possible i dont need
SP1 : Error message from SP4
SP2 : Error message from SP4
SP3 : Error message from SP4
rows in the error table.
What am i doing wrong, what is best practise for error handling ?
And sometimes i also get an error that
The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION

You can try something like this. Only the last nested transaction will record it.
However I don't think this solution is good if the code calling SP1 creates its own transaction. ##TRANCOUNT will be >1 and the code will have to handle the logs.
BEGIN CATCH
Declare #count int
Set #count = ##TRANCOUNT
ROLLBACK TRANSACTION;
...
If (#count = 1)
Begin
INSERT INTO ErrorLog (ObjectName...)
VALUES (OBJECT_SCHEMA_NAME(##PROCID) + '.' + OBJECT_NAME(##PROCID)...)
End
RAISERROR(#ErrorMessage, #ErrorSeverity, #ErrorState);
END CATCH

Related

FireDAC TFDScript error trying to drop a non-existent table

I have a nasty error when executing a Firedac TFDScript error trying to drop a non-existent table:
Delphi Berlin 10.1 Upd 2
Database Firebird 2.5
It give an error when calling FDScript.ExecuteAll (it passes the FDScript.ValidateAll without any problem)
The code I am executing is as follows:
FDScript: TFDScript;
{...}
begin
FDScript.ScriptOptions.Reset;
FDScript.SQLScripts.Clear;
FDScript.SQLScriptFileName := '';
FDScript.ScriptOptions.CommandSeparator := ';';
FDScript.ScriptOptions.CommitEachNCommands := 1;
FDScript.ScriptOptions.DropNonexistObj := True; // seems to ignore this directive
FDConnection.StartTransaction;
try
FDScript.SQLScripts.Add.SQL.Add('drop table countries;');
FDScript.ValidateAll; // no errors here
ScriptStatus := GetEnumName(TypeInfo(TFDScriptStatus), Ord(FDScript.Status));
if FDScript.Status = ssFinishSuccess then begin
FDScript.ExecuteAll; // ERROR HERE! TABLE COUNTRIES DOES NOT EXIXTS
if FDScript.TotalErrors = 0 then begin
FDConnection.Commit;
end
else begin
FDConnection.Rollback;
end;
end
else begin
FDConnection.Rollback;
end;
except
FDConnection.Rollback;
raise;
end;
end;
Implementation seems to be correct. The engine checks if a raised exception is of ekObjNotExists kind, and if so and the DropNonexistObj property is enabled and command kind is DROP or ALTER, it logs to its console. Otherwise re-raises the caught exception.
The only explanation is then, that you are seeing an exception message dialog shown by debugger. These dialogs are displayed even for handled exceptions (so long you won't add them to an ignore list or turn this feature off, which you should not do).

Recover TADOQuery in State dsInsert after disconnect

We use a Delphi TADOQuery with an explicit connection for inserts.
Summary:
When the connection is lost while the query is in State dsInsert, the query seems to enter an inconsistent state with respect to the underlying ADO recordset. As a result, the query cannot be used anymore, even if the connection has been reestablished.
Details:
Assume the following simplified steps:
quTest.Connection:= ADOConnection1;
quTest.Open;
quTest.Insert;
//Simulate lost connection
ADOConnection1.Close;
try
//quTest.State is still dsInsert
quTest.Post; //Throws 'Operation is not allowed when the object is closed'. This is the expected beavior.
except
//Reconnect (simplified algorithm)
ADOConnection1.Connected:= true;
end;
//quTest.State is still dsInsert
//So far, so good.
//Now let's close or abort or somehow reset quTest so that we can use it again. How?
quTest.Close //throws 'Operation is not allowed when the object is closed'
The problem is that at the end of the code sample above quTest is still in State dsInsert, but the underlying ADO recordset is disconnected.
Any attempt to close or somehow reset quTest fails with an exception 'Operation is not allowed when the object is closed'.
Please note that our goal is not to continue the initial insert operation. We just want to bring the query back to a state where we can open and use it again.
Is this possible?
Since quTest is part of a datamodule with design-time field bindings, we cannot easily free the broken query and create a new query instance.
Edit:
Of course, the simulation of the disconnect is not too realistic.
However, comparing the stack traces of the production error and the test sample, we see that the test is good enough.
Production stack trace:
================================================================================
Exception class : EOleException
Exception message: Operation is not allowed when the object is closed
EOleException.ErrorCode : -2146824584
================================================================================
[008B9BD7] Data.Win.ADODB.TCustomADODataSet.InternalGotoBookmark + $17
(0000E290) [0040F290]
[008B9BD7] Data.Win.ADODB.TCustomADODataSet.InternalGotoBookmark + $17
[008B9BF4] Data.Win.ADODB.TCustomADODataSet.InternalSetToRecord + $14
[0081EEBE] Data.DB.TDataSet.InternalSetToRecord + $2
[0081D576] Data.DB.TDataSet.SetCurrentRecord + $62
[0081D9A4] Data.DB.TDataSet.UpdateCursorPos + $10
[0081E378] Data.DB.TDataSet.Cancel + $68
[0081AA49] Data.DB.TDataSet.SetActive + $AD
[0081A841] Data.DB.TDataSet.Close + $9
Test case stack trace:
Data.Win.ADODB.TCustomADODataSet.InternalFirst
Data.DB.TDataSet.SetCurrentRecord(0)
Data.DB.TDataSet.UpdateCursorPos
Data.DB.TDataSet.Cancel
Data.DB.TDataSet.SetActive(???)
Data.DB.TDataSet.Close
In fact, since the query state is still dsInsert, an attempt to .Cancel is made, resulting in subsequent calls to the ADO recordset which fail.
procedure TDataSet.SetActive(Value: Boolean);
begin
...
if State in dsEditModes then Cancel;
...
end;
Edit 2:
The problem is not easy to reproduce, since it seems to be data dependent.
That's why I created a console test program.
Please run the test program twice and change the test case in the main block.
The output of the tests at my machine is shown below.
Console test program:
program Project2;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils,
Data.DB,
Data.Win.ADODB,
ActiveX;
procedure Setup(aConnection: TADOConnection; aEmpty: Boolean);
var
query: TADOQuery;
begin
query:= TADOQuery.Create(nil);
try
query.Connection:= aConnection;
//Create test table
try
query.SQL.Add('create table test3 (a int)');
query.ExecSQL;
WriteLn('Table created.');
except
on e: Exception do
Writeln(e.Message);
end;
//Clear test table
query.SQL.Clear;
query.SQL.Add('delete test3');
query.ExecSQL;
if not aEmpty then begin
//Create a row
query.SQL.Clear;
query.SQL.Add('insert into test3 values (0)');
query.ExecSQL;
end;
finally
query.Free;
end;
end;
var
con: TADOConnection;
query: TADOQuery;
begin
CoInitialize(nil);
try
con:= TADOConnection.Create(nil);
query:= TADOQuery.Create(nil);
try
con.ConnectionString:= 'Provider=SQLOLEDB.1;Persist Security Info=False;Integrated Security=SSPI;Data Source=10.0.0.11,1433;Initial Catalog=TestDB';
con.Connected:= true;
//Test case 1: With data
Setup(con, false);
//Test case 2: No data
//Setup(con, true);
query.Connection:= con;
query.SQL.Add('select * from test3');
query.Open;
query.Insert;
con.Close;
WriteLn('query.Active: ' + BoolToStr(query.Active));
WriteLn('query.State: ' + IntToStr(Ord(query.State)));
query.Close;
WriteLn('Test ran without exception.');
except
on E: Exception do
Writeln('Exception: ' + E.ClassName, ': ', E.Message);
end;
finally
ReadLn;
query.Free;
con.Free;
end;
end.
Test-Environment:
Delphi 10 Seattle Version 23.0.21418.4207
Console test program platform: Win32
Microsoft SQL Server 2008 R2 (SP1) - 10.50.2550.0 (X64)
Tested on:
Windows 8.1 Pro in IDE
Windows 8.1 Pro
Windows Server 2008 R2 Standard, 6.1.7601 SP1 Build 7601
Windows Server 2008 R2 Standard
Output of test case 1:
There is already an object named 'test3' in the database
query.Active: 0
query.State: 0
Test ran without exception.
Output of test case 2:
There is already an object named 'test3' in the database
query.Active: -1
query.State: 3
Exception: EOleException: Operation is not allowed when the object is closed
I don't like posting an answer which doesn't actually answer the question, but
in this case I think I should because I simply cannot reproduce what you've said in
your comments regarding the state of your quTest. Perhaps the divergence
between my results and yours is due to some part of your code or object properties
that are not included in your question.
Please try this (I've tested it in D7 and Seattle):
Start a new project and drop a TAdoConnection and TAdoQuery on your form.
Make only the property changes shown in the DFM extract below;
Set up the event handlers shown in the code extract shown below.
Put breakpoints inside the BeforeClose and BeforeCancel handlers, and one on
quTest.Post
then compile, run and click Button1.
What I get is as follows:
The BP on BeforeClose trips.
The BP on BeforeCancel trip.
The BP on quTest.Post trips.
At step 3, the state of quTest is dsInactive and its Active property is False.
Those values and the fact that the Before ... events are called beforehand are
precisely what I would expect, given that calling AdoConnection.Close closes
the dataset(s) using it as their Connection.
So, I think that if you get different results with your app, you need to explain
why because I think I've shown that a test project does not exhibit the
behaviour you've reported.
Update 2
At the request of the OP, I added an int column 'a' to the table, and a corresponding Parameter to quTest and added
quTest.Parameters.ParamByName('a').Value:= 0;
before both my calls to quTest.Open. This makes no difference to the State and Active properties of quTest when the BP on quTest.Post trips: they are still dsInactive and False, respectively.
As the OP has said he just want to be able to carry on using quTest after the aborted Insert, I replaced quTest.Post; in the except block by quTest.Open;. After that, once the Inssert exception has occurred, I can carry on using quTest without any apparent problem - I can do Deletes, Inserts and Edits manually and these are correctly passed back to the server, so that when the app is re-run, these changes have persisted.
Update 3. The OP seems to be in some doubt that calling AdoConnection1.Close results in quTest being closed. It does. To verify this put a watch on Form1.quTest.RecordSetState and run the appl as far as AdoConnection1.Close. Then, trace into that call. You will find that TCustomConnection.SetConnected calls DoDisconnect
which calls ConnectionObject.Close. That sets quTest.RecordSetState to stClosed so that when TAdoConnection.Disconnect executes
for I := 0 to DataSetCount - 1 do
with DataSets[I] do
if stClosed in RecordsetState then Close;
quTest is closed.
Sample code
TForm1 = class(TForm)
ADOConnection1: TADOConnection;
quTest: TADOQuery;
Button1: TButton;
procedure FormCreate(Sender: TObject);
procedure Button1Click(Sender: TObject);
procedure quTestBeforeCancel(DataSet: TDataSet);
procedure quTestBeforeClose(DataSet: TDataSet);
public
{ Public declarations }
procedure TestReconnect;
end;
[...]
procedure TForm1.FormCreate(Sender: TObject);
begin
quTest.Open;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
TestReconnect;
end;
procedure TForm1.quTestBeforeCancel(DataSet: TDataSet);
begin
Caption := 'Before Cancel';
end;
procedure TForm1.quTestBeforeClose(DataSet: TDataSet);
begin
Caption := 'Before close';
end;
procedure TForm1.TestReconnect;
begin
quTest.Connection:= ADOConnection1;
quTest.Open;
quTest.Insert;
//quTest.FieldByName('Name').AsString := 'yyyy'; added by MA
//Simulate lost connection
ADOConnection1.Close;
try
quTest.Post; //Throws 'Operation is not allowed when the object is closed'
except
//Reconnect (simplified algorithm)
ADOConnection1.Connected:= true;
quTest.Post;
end;
end;
end.
Partial DFM
object ADOConnection1: TADOConnection
Connected = True
ConnectionString =
'Provider=SQLOLEDB.1;Persist Security Info=False;User ID=sa;Initia' +
'l Catalog=MATest;Data Source=MAI7'
Provider = 'SQLOLEDB.1'
Left = 24
Top = 24
end
object quTest: TADOQuery
Connection = ADOConnection1
CursorType = ctStatic
BeforeClose = quTestBeforeClose
BeforeCancel = quTestBeforeCancel
Parameters = <>
SQL.Strings = (
'Select * from TestTable')
Left = 64
Top = 24
end
Update 1 The following code allows the completion of the pending insert
in the except block. Note the absence of a call to quTest.Post in the except block.
procedure TForm1.TestReconnect;
const
SaveFileName = 'C:\Temp\testdata.xml';
begin
quTest.Connection:= ADOConnection1;
quTest.Open;
quTest.Insert;
quTest.FieldByName('Name').AsString := 'yyyy';
quTest.SaveToFile(SaveFileName, pfXML);
//Simulate lost connection
ADOConnection1.Close;
try
quTest.Post; //Throws 'Operation is not allowed when the object is closed'
except
//Reconnect (simplified algorithm)
ADOConnection1.Connected:= true;
quTest.LoadFromFile(SaveFileName);
end;
end;
The reason for the observed behavior is a change in or before Delphi XE6, which I think is a bug.
https://quality.embarcadero.com/browse/RSP-15545
Summary:
The problem does not occur in Delphi 2007 and Delphi XE.
The problem occurs in Delphi 10.1
The problematic code change has been introduced in or before XE6 in TDataSet.SetActive, where a new call to Cancel has been added.
This call fails in the described scenario leading to the described effects.

ADS error 5047 with client dataset

Reported Problem:
exception class : AdsError
exception message : Error 5047: The transaction command was not in valid sequence. Not in a transaction.
main thread ($918):
00607cc5 +07d Boxer.exe adscnnct 1576 +20 TAdsConnection.PerformRollback
00607d52 +002 Boxer.exe adscnnct 1606 +0 TAdsConnection.Rollback
0062eab5 +09d Boxer.exe DModule1 241 +10 TDM1.ApplyBoxer2Changes
Offending code:
procedure TDM1.ApplyBoxer2Changes;
begin
AdsConnection.BeginTransaction;
try
if cdsBoxes.ApplyUpdates(0) = 0 then
AdsConnection.Commit
else
raise Exception.Create('Record changed by another user.');
except
on e: Exception do
begin
AdsConnection.Rollback;
MessageDlg('An error occurred while attempting to save your changes:'+#13#10#10
+e.Message, mtError, [mbOK], 0);
cdsBoxes.CancelUpdates;
end;
end;
cdsBoxes.Refresh;
end;
Where cdsBoxes is a TClientDataSet connected to a TDataSetProvider whose DataSet is a TAdsQuery with the same AdsConnection = TDM1.AdsConnection, and the following SQL:
SELECT * FROM Boxer2 WHERE Boxer1Id=:id
Can anyone see what is wrong with this code?
The 5047 error appears to be indicating that the transaction is no longer active when the Rollback method is called.
I haven't been able to reproduce the error myself; in my testing I'm able to add, edit and delete multiple records and then all the changes are either committed or the rollback occurs successfully.
Update: I have been able to reproduce it. The problem occurs when there are 2 identical records in the table, so it is impossible to identify which one to update (there is no unique key on the table, DataSetProvider.UpdateMode = upWhereAll).
I'm not sure why the transaction is being cancelled automatically in that case, but a workaround to at least present a user-friendlier message is as follows:
procedure TDM1.ApplyBoxer2Changes;
begin
AdsConnection.BeginTransaction;
try
if cdsBoxes.ApplyUpdates(0) = 0 then
AdsConnection.Commit
else
raise Exception.Create('Record changed by another user.');
except
on e: Exception do
begin
if AdsConnection.TransactionActive then
begin
AdsConnection.Rollback;
MessageDlg('An error occurred while attempting to save your changes:'+#13#10#10
+e.Message, mtError, [mbOK], 0);
end
else
begin
MessageDlg('A data integrity error occurred while attempting to save your changes.'+#13#10#10
+'Please report this to the help desk.', mtError, [mbOK], 0);
end;
cdsBoxes.CancelUpdates;
end;
end;
cdsBoxes.Refresh;
end;

syntax error(missing operator) in query expression when using delphi and ms access

Does anyone know the source of this error:
project xxxx.exe raised exception class EoleException with message
'syntax error(missing operator) in query expression 'serial number=?'
this is my actual code in delphi with ms access database.
with AddIndividualsAccountADOQuery do
begin
SQL.Clear;
SQL.Add('Select * from IndividualAccount where Serial Number=:Sno');
Parameters.ParamByName('Sno').Value:=edit1.Text;
Open;
Active:= True;
end;
the actual code runs well with delphi and sql database. but when i use ms access, the above error results when i run the application.
I believe with MS-Access you need brackets around the name (since it contains a space):
Select * from IndividualAccount where [Serial Number]=:Sno

Problem with using transaction in delphi when calling to MS SQL SERVER 2008 R2?

I need to call some Stored Procedures from Delphi and because they are related I have to use transactions.
But It always returns an error when called :
'Transaction cannot have multiple recordsets with this cursor type. Change the cursor type ,commit the transaction, or close one of the recordsets.'
And this error only occurs for MS SQL SERVER 2008, when I use MS Access It works fine.
Whats the problem ?
Thanks in advance
UPDATE :
procedure TForm1.Button2Click(Sender: TObject);
begin
if not DM.ADOConnection.InTransaction then
dm.ADOConnection.BeginTrans;
ADOQuery.LockType := ltBatchOptimistic;
ADOQuery.CursorType := ctUnspecified;
Try
with ADOQuery do
begin
Close;
SQL.Clear;
SQL.Text := 'INSERT INTO [UserAction] (UAct_Frm_ID,UAct_Type,UAct_Description'
+',UAct_Date,UAct_Time,UAct_Usr_ID)'
+'VALUES(:UAct_Frm_ID'
+',:UAct_Type,:UAct_Description,:UAct_Date,:UAct_Time'
+',:UAct_Usr_ID)';
Parameters.ParamByName('UAct_Frm_ID').Value := 1;
Parameters.ParamByName('UAct_Type').Value := 1;
Parameters.ParamByName('UAct_Description').Value := 'test by Q1';
Parameters.ParamByName('UAct_Date').Value := completdate(datenow);
Parameters.ParamByName('UAct_Time').Value := TimeToStr(Now);
Parameters.ParamByName('UAct_Usr_ID').Value := 1;
ExecSQL;
end;
Except
DM.ADOConnection.RollbackTrans;
ShowMessage('RollBack');
Exit;
End;
dm.ADOConnection.CommitTrans;
ShowMessage('Commite');
end;
From here:
Resolution:
Use a different cursor type, change
the cursor location to adUseClient or
close the first recordset before
opening another on the same
connection/transaction.
Cause:
SQL Server can only open one
ForwardOnly cursor at a time on a
connection, because SQL Server can
only process one active statement at a
time per connection.
When you try to open more than one
ForwardOnly ADO recordset at a time on
a single Connection, only the first
ADO recordset is actually opened on
the Connection object. New, separate
connections are created for subsequent
ForwardOnly cursors.
A transaction is on a single
connection. When you attempt to open
more than one ForwardOnly recordset
within a single transaction, ADO
attempts to open more than one
ForwardOnly recordset on the
connection of the transaction. An
error occurs because SQL Server only
allows one ForwardOnly recordset on a
single connection. Because the error
is within a manual transaction, you
might see the error above. Microsoft
Data Access Objects 2.1 Service Pack 2
and later versions of MDAC contain
more informative error messages. For
that reason, you may see the more
informative second or third error
message, above.
Try with including [eoExecuteNoRecords] into ExecuteOptions.

Resources