Recover TADOQuery in State dsInsert after disconnect - delphi

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.

Related

FireDAC - How to execute Stored procedure with default parameters?

i have created following stored procedure on MS SQL Server
CREATE PROCEDURE sppl_ParamTest
#ID int = 666
AS
BEGIN
SELECT #ID;
END
And trying to call it with FireDAC (without creating any parameters):
FCommand:TFDCommand;
...
FCommand.Params.Clear;
FCommand.SQL.Text:='sppl_ParamTest';
FCommand.CommandKind:=skStoredProc
if FCommand.Params.Count=0 then
FCommand.Open;
But stored procedure returns NULL(supose to return 666)
As i understood, its caused by FireDAC magic power to query each procedure meta data, before procedure actually calls.
Is it possible to solve this ?
I added your definition of sppl_ParamTest to my Sql Server 2014 and executed the following code after minimal configuration of an FDConnection to point it at the server & database on it. The FDQuery is fresh from the component palette, with only its Connection property set.
procedure TForm1.FormCreate(Sender: TObject);
begin
FDQuery1.SQL.Text := 'sppl_ParamTest';
FDQuery1.Open;
Caption := FDQuery1.Fields[0].AsString;
end;
The form's caption displays 666 as expected.
Partial DFM (user name and password omitted):
object FDConnection1: TFDConnection
Params.Strings = (
'Database=MATest'
'Server=MAT410\ss2014'
'DriverID=MSSQL')
LoginPrompt = False
Left = 32
Top = 16
end
object FDQuery1: TFDQuery
Connection = FDConnection1
Left = 104
Top = 16
end
Update I think that the OP's problem is most likely arising because of a flaw in the implementation of TFDCommand and TFDStoredProc. Executing
sppl_ParamTest default
in Sql Server Management Studio correctly returns
666
So does opening an FDQuery with sppl_ParamTest default (or just sppl_ParamTest) as its command text.
However, with a TFDCommand's CommandText set to the same value, calling Execute on it produces this error
Could not find Stored Procedure 'MATest.dbo.sspl_ParamTest default'
Equally, doing
FDStoredProc1.ParamByName('#ID').Value := 'default';
(unsurprisingly) provokes the error
Could not convert variant of type (String) into type (Integer)
on calling FDStoredProc.Open and has so far resisted my attempts to set a parameter type which will accept 'default' and correctly execute the sp.
Also, attempts to invoke the FDCommand with EmptyParam as the input parameter value fail to return the 666 value.
Setting this options to
FetchOptions.Items:=[]
or
FetchOptions.Items := FetchOptions.Items - [fiMeta]
Will prevent FireDac from querying meta data, and will allow to use param defaults.
Thanks to whosrdaddy

Calling TJSONUnMarshal.Unmarshal on a marshalled TStringlist causes memory corruption?

This q is to provide an MCVE to illustrate a problem uncovered by the answer to this earlier one of mine:
Unexpected failure of custom registered Reverter using TJSONUnMarshal
In my earlier q, the answer from #VirusTrinity fixed the problem I was initially having,
namely that the object obtained by the unmarshalling operation was incorrectly returned
with a Nil TStringlist field. However, the fix uncovered a new problem, which is that
on shutdown of the application, it generates a 0xC0000005 exception, access violation at
address 0x00000000 read of address 0x00000000. The code below shows this behaviour, generating
the same AV on shutdown, with the same cause.
The AV evidently occurs due to memory corruption during execution of PopulateFields(objFields, Obj, customizer); in function TJSONUnMarshal.CreateObject(JsonObj: TJSONObject): TObject in Data.DBXJSONReflect.
If you put a breakpoint where shown below in the code, add a watch on
TEncoding.FAnsiEncoding and then single-step through
AnObject:= jUnmarshal.Unmarshal(jValue);
you should find that the value in the FMaxCharSize changes from 1 to 5 (sometimes
in the previous q I got 7 instead of 5). This change seems to be symptomatic of
the memory corruption which apparently causes the AV on shutdown.
Update: To see where the AV occurs, place a breakpoint on the entry point to class procedure TEncoding.FreeEncodings. For me, the AV occurs when
FreeAndNil(FANSIEncoding);
is called.
Update #2: The point where TEncoding.FAnsiEncoding gets corrupted is when function TJSONUnMarshal.ClassTypeOf is called with the Field parameter having the value FDelimiter and executes the statement
fRtti := tRtti.GetField(Field);
The call to ClassTypeOf is from the statement
else if HasReverter(ComposeKey(ClassTypeOf(Data, FieldName),
FIELD_ANY)) then
at line 30222/3023 of procedure TJSONUnMarshal.PopulateFields
Could anyone confirm the problem and advise why it is occurring?
Code:
program MATest;
{$APPTYPE CONSOLE}
uses Classes, System.SysUtils, JSon, DBXJson, DBXJSONReflect;
procedure Test;
var
TL1,
TL2 : TStringlist;
AnObject : TObject;
jMarshal: TJSONMarshal;
jUnMarshal : TJSonUnMarshal;
jValue: TJSONValue;
begin
TL1 := TStringlist.Create;
TL1.Add('AAA');
try
jMarshal := TJSONMarshal.Create(TJSONConverter.Create);
jValue := jMarshal.Marshal(TL1);
Writeln(jValue.ToString);
jUnMarshal := TJSONUnMarshal.Create; // < Put breakpoint here and
// add watch of TEncoding.FAnsiEncoding in System.SysUtils
AnObject:= jUnMarshal.UnMarshal(jValue);
TL2 := TStringlist(AnObject);
Writeln(TL2.Text);
Readln;
finally
jValue.Free;
jMarshal.Free;
jUnMarshal.Free;
TL1.Free;
TL2.Free;
end;
end;
begin
try
Test;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end.

Sharing an ADO Connection across a DLL boundary

We would like to share an ADOConnection across a DLL boundary (Delphi to Delphi at the moment, though could also be C# to Delphi in the near future).
As we would like the flexibility to call the DLL from c# in future, we were hoping to be able to define the DLL call using _Connection as a parameter. Something like:
procedure DoStuff (ADOConnection: _Connection)
var
InnerConnection: TADOConnection;
begin
InnerConnection := TADOConnection.create(nil);
try
InnerConnection.ConnectionObject := ADOConnection;
DoMoreStuff(InnerConnection);
finally
InnerConnection.free;
end;
end;
Unfortunately, the TADOConnection destructor code closes the connection passed into it, which is an unwanted side-effect. Adding
InnerConnection.ConnectionObject := nil
prior to the free doesn't do anything, as it's caught by
if Assigned(Value) = nil
in TADOConnection.SetConnectionObject, which results in the call not doing anything.
Is there a better way of achieving this? Passing the connection string is an alternative, but would mean that we would have to deal with username/password issues and encryption across the boundary. Passing the TADOConnection is another option, but that prevents calling from other languages.
Edit: For clarity, the Username/Password of the original TADOConnection object is set using the .Open routine, so these details aren't in the connection string (in fact, the wrong username is usually stored, as it's the name used to 'test connection' in the MS UDL editor)
You can try this way:
type TInit_StFattDLL = procedure( var DataBase:TAdoConnection);
var Init_StFattDLL:TInit_StFattDll;
The caller is:
Function ConnectDll():Boolean;
var
handleDll:THandle;
begin
handleDll := LoadLibrary('mydll.DLL');
#Init_StFattDLL := GetProcAddress(handleDll , 'myConnectFunction');
if #Init_StFattDLL <> nil then
begin
Init_StFattDLL(ADOConnection1);
result:=true;
end
else
result:=false;
end;
into the the dll put the following:
in the project file put the exports:
Exports myConnectFunction;
global section:
var Database:TAdoConnection;
the exported procedure is the following:
procedure myConnectFunction( var MyDataBase:TAdoConnection);export;
begin
Database:=MyDataBase;
end

invalid class typecast in intraweb

I'm porting an old application from delphi7 to delphi xe8
and from intraweb 8 to intraweb XIV
My app was subdivided in a main program and a number of child packages
and it worked perfectly with the old components.
With theese new components, I now get an exception trying to generate and return a page to the server controller, creating such a page from a child package.
If instead I generate the page from the main app, it works.
In the procedure TIWServerController.IWServerControllerBaseGetMainForm
I call a procedure of a my component (packman) that tries to obtain a main window from a child package.
this is the servercontroller function
procedure TIWServerController.IWServerControllerBaseGetMainForm(var vMainForm : TIWBaseForm);
begin
VMainForm := PackMan.MainLoginForm(webApplication);
end;
and this is the packman function:
function tPackMan.MainLoginForm (aOwner:tComponent) : tIwAppForm;
var Proc : tGetMainFormProc;
begin
#Proc := GetProcAddress (LoginPkg,'MainForm');
Result := Proc(aOwner);
end;
this is the definition of the procedural type:
tGetMainFormProc = function (aOwner:tComponent): tIwAppForm;
and this is the MainForm procedure, in the child package (packlogin).
Initially I tried to create the original form, full of components,
after that I've removed all components from original form, without success,
and finally I tried to construct an empty form, as shown in this sample:
function MainForm (aOwner:tComponent): tIWAppForm;
begin
Result := tIWAppForm.Create(aOwner);
end;
exports MainForm;
I've traced the program behaviour using several Outputdebugstring messages (here not shown) and I've come to the following conclusion:
1) the Mainform procedure, in the package, seems to return a valid tIwAppform
2) this Object is correctly returned to the IWServerControllerBaseGetMainForm procedure
and the variable VMainForm is correctly assigned.
3) if I inspect the classname property of this variable, I see it has the value "tIWAppform".
The exception seems to be generated at the procedure return.
I've interceped it in the procedure IWServerControllerBaseException
with the following code :
procedure TIWServerController.IWServerControllerBaseException(
AApplication: TIWApplication; AException: Exception;
var Handled: Boolean);
begin
Dump ('UNEXPECTED EXCEPTION ' + AException.message);
Handled := true;
end;
What am I missing ?
Any suggestion ?
Regards.
Maurizio.

Microsoft AlwaysOn failover solution and Delphi

I'm trying to make a Delphi application to work with AlwaysOn solution. I found on Google that I have to use MultiSubnetFailover=True in the connection string.
Application is compiled in Delphi XE3 and uses TADOConnection.
If I use Provider=SQLOLEDB in the connection string, application starts but it looks like MultiSubnetFailover=True has no effect.
If I use Provider=SQLNCLI11 (I found on Google that OLEDB doesn't support AlwaysOn solution and I have to use SQL Native client) I get invalid attribute when trying to open the connection.
The connection string is:
Provider=SQLOLEDB.1;Password="password here";Persist Security Info=True;User ID=sa;Initial Catalog="DB here";Data Source="SQL Instance here";MultiSubnetFailover=True
Do I have to upgrade to a newer version on Delphi to use this failover solution or is something that I'm missing in the connection string?
I am currently using XE2 with SQL Server AlwaysOn. If you read the documentation you will see that AlwaysOn resilience events will cause your database connection to fail and you need to initiate a new one.
If a SqlClient application is connected to an AlwaysOn database that
fails over, the original connection is broken and the application must
open a new connection to continue work after the failover.
I've dealt with this via the simple expedient of overriding the TAdoQuery component with my own version which retries the connection after getting a connection failure. This may not be the proper way to do this but it certainly works. What it does is override the methods invoked for opening (if the query returns a result set) or executes the SQL (otherwise) and if there is a failure due to connection loss error tries again (but only once). I have heavily tested this against AlwaysOn switch overs and it works reliably for our configuration. It will also react to any other connection loss events and hence deals with some other causes of queries failing. If you are using a component other than TAdoQuery you would need to create similar overrides for that component.
It is possible this can be dealt with in other ways but I stopped looking for alternatives once I found something that worked. You may want to tidy up the uses statement as it clearly includes some stuff that isn't needed. (Just looking at this code makes me want to go away and refactor the code duplication as well)
unit sptADOQuery;
interface
uses
Windows, Messages, SysUtils, Classes, Db, ADODB;
type
TsptADOQuery = class(TADOQuery)
protected
procedure SetActive(Value: Boolean); override;
public
function ExecSQL: Integer; // static override
published
end;
procedure Register;
implementation
uses ComObj;
procedure Register;
begin
RegisterComponents('dbGo', [TsptADOQuery]);
end;
procedure TsptADOQuery.SetActive(Value: Boolean);
begin
try
inherited SetActive(Value);
except
on e: EOleException do
begin
if (EOleException(e).ErrorCode = HRESULT($80004005)) then
begin
if Assigned(Connection) then
begin
Connection.Close;
Connection.Open;
end;
inherited SetActive(Value); // try again
end
else raise;
end
else raise;
end;
end;
function TsptADOQuery.ExecSQL: Integer;
begin
try
Result := inherited ExecSQL;
except
on e: EOleException do
begin
if (EOleException(e).ErrorCode = HRESULT($80004005)) then
begin
if Assigned(Connection) then
begin
Connection.Close;
Connection.Open;
end;
Result := inherited ExecSQL; // try again
end
else raise;
end
else raise;
end;
end;
end.

Resources