FireDAC TFDScript error trying to drop a non-existent table - delphi

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).

Related

Get exception text from FireDAC debugger notification

I'm trying to capture the debugger notification text, Firedac connection
//FDScript1.SQLScripts.Add.SQL.LoadFromFile('C:\SGI\Rodar_Apos_Atualizar_3.txt');
//FDScript1.ValidateAll;
//FDScript1.ExecuteAll;
//MSScript1.SQL.LoadFromFile('C:\SGI\Rodar_Apos_Atualizar_3.txt');
//MSScript1.Execute;
try
FDConnection1.Connected := True;
FDScript1.SQLScripts.Add.SQL.LoadFromFile('C:\SGI\Rodar_Apos_Atualizar_3.txt');
FDScript1.ExecuteAll;
FDScript1.ValidateAll;
except
//EMSSQLNativeException
on E: EMSSQLNativeException do
begin
//ShowMessage('Erro'+FDGUIxErrorDialog1.ErrorDialog.Caption);
//Memo1.Clear;
Memo1.Text := 'Erro de Sistema: '+#13#10+ E.Message;
end;
end;
It really would help if you would show us the scripts you are trying to execute,
or at least the script which does not execute correctly. In any case, your code is wrong
because the documentation
states
It is good practice to call the ValidateAll method before the ExecuteAll method.
Note the 'before'. Your ValidateAll is after ExecuteAll, not before. Both are Boolean
functions but you are not checking their results, which you should.
With some trivial experimenting I found that I can provoke a EMSSQLNativeException
using SqlServer 2014 with the code below:
procedure TForm2.Button1Click(Sender: TObject);
const
sScript = 'select m.*, d.* from master m join detail d on, m.masterid = d.masterid';
var
FDScript : TFDSqlScript;
begin
try
FDConnection1.Connected := True;
FDScript := FDScript1.SQLScripts.Add;
FDScript.SQL.Text := sScript;
//FDScript1.ExecuteAll;
//FDScript1.ValidateAll;
// FDScript1.ValidateAll then
//FDScript1.ExecuteAll;
FDQuery1.SQL.Text := sScript;
FDQuery1.Open();
except
//EMSSQLNativeException
on E: EMSSQLNativeException do
begin
//ShowMessage('Erro'+FDGUIxErrorDialog1.ErrorDialog.Caption);
//Memo1.Clear;
Memo1.Text := 'Erro de Sistema: '+#13#10+ E.Message;
end;
end;
end;
Note the blatantly wrong syntax in the Sql statement, namely the comma after the on.
When FDQuery1.Open is called, this exception is raised (and caught initially by the debugger)
---------------------------
Debugger Exception Notification
---------------------------
Project sqlerror.exe raised exception class EMSSQLNativeException with message '[FireDAC][Phys][ODBC][Microsoft][SQL Server Native Client 11.0][SQL Server]Incorrect syntax near 'd'.'.
---------------------------
Break Continue Help
---------------------------
When I click Continue, execution proceeds into your exception handler, exactly as #TomBrunberg described in a comment
and the exception's message text is inserted into Memo1.
So I cannot reproduce the behaviour you describe based on the information in your question. It
must be caused by something you are not telling us, possibly in code you have
not included in your q or some property setting of the components you are
using.
Hopefully, trying the code above, you will find the debugger behaving as I
have described and this may give you some clue as to why you are getting the
problem you've described. Note that it is very important that you try the code
above in a new project, not your existing one. The only property setting
you need to do before executing it is to set FDConnection1 so that it can connect
to your server.
FWIW, if I uncomment-out the lines
FDScript1.ValidateAll then
FDScript1.ExecuteAll;
they execute without complaint, and I get the exact same behaviour as i've described without them.

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.

How to set up 2 IBDatabase to 1 IBTransaction?

I have one IBDatabase in DataModule linked with my IBTransaction.
In one module of project I need to control the persistence in two database.
For this, I am adding the second IBDatabase this way:
constructor TConnections.Create(AIBDatabase: TIBDatabase);
begin
if AIBDatabase = nil then
raise Exception.Create('The base connection is needed!');
inherited Create;
FIBDatabase := TIBDatabase.Create(nil);
FIBDatabase.LoginPrompt := false;
FIBDatabase.Params.Clear;
FIBDatabase.Params.Text := AIBDatabase.Params.Text;
FIBDatabase.DatabaseName := AIBDatabase.DatabaseName.Replace('DB.GDB', 'DB2.GDB');
end;
procedure TConnections.SetTransaction(AIBTransaction: TIBTransaction);
begin
if AIBTransaction = nil then
raise Exception.Create('Then Transaction is needed!');
AIBTransaction.AddDatabase(FIBDatabase);
FIBDatabase.DefaultTransaction := AIBTransaction;
FIBDatabase.Open;
end;
Any select commands are work fine, but in insert command the error occurs.
Well, I have this:
connections := TConnections.Create(Dm.Database);
try
connection.SetTransaction(Dm.Transaction);
qry := TIBQuery.Create(nil);
qry.Database := Dm.Database;
try
// here are commands with Dm.Transaction
// ...
qry.ExecSql;
finally
qry.Free;
end;
otherQry := TIBQuery.Create(nil);
otherQry.Database := connection.OtherDatabase;
try
// here are commands with connection.OtherDatabase but same Transaction
// ...
otherQry.ExecSql; // The error occurs here.
finally
otherQry.Free;
end;
Dm.Transaction.Commit;
finally
connection.Free;
end;
'invalid transaction handle (expecting explicit transaction start)'
These block is envolved in try except.
So, if I try again, after the error, the process runs smoothly.
What's wrong in my configuration?
This may occur if you started transaction explicitly. Every explicit transactions must be finished explicitly. So, if your connection is open explicitly, you should close it explicitly.
You may use :
//Commit(Stop) the transaction before open an other connection
if Dm.Transaction.InTransaction then
dm.Transaction.Commit;
Note: In applications that connect an InterBaseExpress dataset to a client dataset, every query must be in its own transaction. You must use one transaction component for each query component.
http://docwiki.embarcadero.com/Libraries/XE8/en/IBX.IBDatabase.TIBTransaction

User friendly exception error delphi

I have this method where i execute a sql statement and catch a error in a try except statement
AdoQuery := TAdoQuery.Create(self);
AdoQuery.connection := AdoConnection;
AdoQuery.SQL.Add(sqlStr);
AdoQuery.Prepared := true;
try
begin
AdoQuery.ExecSql;
AdoQuery.Active := false;
end;
except on e:eAdoError do
ShowMessage('Error while creating the table: ' + e.Message);
end;
I can catch the error like this and show it to the user but it's showing some useless info for the user. I Would like to show only the %msg part of the error, take a look at the pic:
I tought e.MEssage allow me to get only the %msg part but it give me the whole thing hardly understoodable by a random user. How do i get only the usefull info in this case
Table reftabtest.rPCE already exists
Thank you.
You can use the Errors property of the TADOConnection object, what you want is the Description member of the Error object.
In your case:
function ParseOBDCError(ErrorDescription : String) : String;
var
Ps : Integer;
Pattern : String;
begin
Pattern := '%msg:';
Ps := Pos(Pattern, ErrorDescription);
if Ps > 0 then
begin
Result := Copy(ErrorDescription, Ps+Length(Pattern)+1);
// if you want, you can clean out other parts like < and >
Result := StringReplace(Result, '<', , '', [rfReplaceAll]);
Result := StringReplace(Result, '>', , '', [rfReplaceAll]);
Result := Trim(Result);
end
else
Result := ErrorDescription;
end;
...
AdoQuery := TAdoQuery.Create(self);
AdoQuery.connection := AdoConnection;
AdoQuery.SQL.Add(sqlStr);
AdoQuery.Prepared := true;
try
AdoQuery.ExecSql;
AdoQuery.Active := false;
except on e : Exception do
begin
if AdoConnection.Errors.Count > 0 then
ShowMessageFmt('Error while creating the table: %s',
[ParseOBDCError(AdoConnection.Errors[0].Description)])
else
ShowMessageFmt('something went wrong here: %s', [e.Message]);
end;
end;
That message dialog wasn't shown by your ShowMessage() code.
First, the icon is wrong - that's not the ShowMessage icon.
Second, the text you added ('Error while creating the table: ') to the message is missing.
This means your exception swallower is not catching the exception, because it's not of the EADOError class. So what's happening is the application's default exception handler is showing the exception.
Before I explain how to fix it, I need to point out that your exception swallower is wrong (your's should not be misnamed an exception handler).
Because you're swallowing the exception: If another method calls yours it will incorrectly think you method succeeded, and possibly do something it shouldn't. You should never write code that makes an assumption that there isn't a significant call stack leading into your method.
Swallowing to show a message to the user doesn't help, because it hides the error from the rest of the program. Especially since, as you can see: there is already code in one place that tells the user about the error without hiding it from the rest of the program. (The problem you have is that you want a friendlier message.)
To fix it:
First find out what the actual exception class is, so you're able to catch the correct error.
Now you have a number of options, but the simplest is as follows:
First log the exception, preferably with call stack. You don't want to be stuck in the situation where the user gets a friendly message but you as developer lose critical information if you need to do some debugging.
To get the call stack you can consider third-party tools like Mad Except, Exceptional Magic, JCLDebug to name a few.
Now show the message to the user.
Finally call Abort. This raises an EAbort exception which by convention is a "silent exception". It tells the rest of the program that there was an error (so it doesn't do things assuming everything is fine). But by convention, any further exception handlers should not show another message to the user. This includes the default handler's dialog in your question.
If the default handler is incorrectly showing EAbort messages, then it should be fixed.

Why is except not catching this error?

I have a program that simulates dice rolls and compares them to values in a chart (set of String lists). I currently get the value from a TEdit. If the box is empty it raises a EConvertError that should be caught by my Try/Except statement, but it's not. Thoughts and advice? Code below, Delphi 7.
try
//Shooting
if ShootingRadio.Checked then
BS := StrToInt(Edit1.Text);
Randomize;
Roll := RandomRange(1,7);
Label3.Caption := IntToStr(Roll);
if (Roll < StrToInt(ShootingHitChart[BS-1])) then
begin
Label3.Caption := (IntToStr(Roll)+' Miss');
RichView1.AddTextNL((IntToStr(Roll)+' Miss'),7,0,1);
RichView1.Reformat;
end
else
begin
Label3.Caption := (IntToStr(Roll)+' Hit');
RichView1.AddTextNL((IntToStr(Roll)+' Hit'),6,0,1);
RichView1.Reformat;
end;
except
MessageBox(0,'No number entered.','Error',mb_OK);
end;
'Stop on Delphi exceptions' is checked in the debugger options. The exception is actually caught just fine, but the IDE stops when you get it. When you continue running, you will not see the exception, but your message instead. Out of the IDE it will run fine.
You can uncheck this option (I usually do). You can always re-check it when you need to debug some stubborn problem.

Resources