4

I have this code that is returning an Access Violation ('Access violation at address 74417E44 in module 'sqloledb.dll'. Read of address 786E3552') and I can't identify where is the problem. My only guess is that ADOQuery has a limit for the number of parameters we can pass. The code is as follows:

With qryInsert do
  begin
    Active := False;
    Close;
    Sql.Clear;
    Sql.Add('Insert Into MyTable(ColumnOne, ');
    Sql.Add('             ColumnTwo,           ');
    Sql.Add('             ColumnThree,         ');
    Sql.Add('             ColumnFour,           ');
    Sql.Add('             ColumnFive,          ');
    Sql.Add('             ColumnSix,        ');
    Sql.Add('             ColumnSeven,        ');
    Sql.Add('             ColumnEight,     ');
    Sql.Add('             ColumnNine,       ');
    Sql.Add('             ColumnTen,       ');
    Sql.Add('             ColumnEleven,     ');
    Sql.Add('             ColumnTwelve,   ');
    if qrySelect.FieldByName('ColumnTwelve').AsSTring = 'Y' then
    begin
      Sql.Add('           ColumnThirteen,   ');
      Sql.Add('           ColumnFourteen,   ');
      Sql.Add('           ColumnFifteen,   ');
    end;
    Sql.Add('             ColumnSixteen,   ');
    if qrySelect.FieldByName('ColumnSixteen').AsSTring = 'Y' then
    begin
      Sql.Add('           ColumnSeventeen,         ');
      Sql.Add('           ColumnEighteen,         ');
      Sql.Add('           ColumnNineteen,         ');
    end;
    if qrySelect.FieldByName('ColumnTwenty').AsSTring = 'Y' then
    begin
      Sql.Add('           ColumnTwenty,  ');
      Sql.Add('           ColumnTwentyOne,        ');
      Sql.Add('           ColumnTwentyTwo,        ');
      Sql.Add('           ColumnTwentyThree,        ');
    end
    else
      Sql.Add('           ColumnTwenty,  ');
    Sql.Add('             ColumnTwentyFour) ');
    Sql.Add('Values(:ColumnOne, :ColumnTwo, :ColumnThree, :ColumnFour, ');
    Sql.Add('       :ColumnFive, ' + dateDB + ', :ColumnSeven,          ');
    Sql.Add('       :ColumnEight, :ColumnNine, :ColumnTen, ');
    Sql.Add('       :ColumnEleven,                                    ');
    Sql.Add('       :ColumnTwelve,                                    ');
    if qrySelect.FieldByName('ColumnTwelve').AsSTring = 'Y' then
      Sql.Add('     :ColumnThirteen, :ColumnFourteen, :ColumnFifteen,              ');
    Sql.Add('       :ColumnSixteen,                                      ');
    if qrySelect.FieldByName('ColumnSixteen').AsSTring = 'Y' then
      Sql.Add('     :ColumnSeventeen, :ColumnEighteen, :ColumnNineteen,                 ');
    if qrySelect.FieldByName('ColumnTwenty').AsSTring = 'S' then
    begin
      Sql.Add('   :ColumnTwenty,                                      ');
      Sql.Add('   :ColumnTwentyOne, :ColumnTwentyTwo, :ColumnTwentyThree,                ');
    end
    else
      Sql.Add('   :ColumnTwenty,                                      ');
    Sql.Add('     :ColumnTwentyFour)                                  ');
    {And then for all the parameteres, pass the value}
    Parameters.ParamByName('ColumnOne').Value := varColumnOne;
    ...
    Parameters.ParamByName('ColumnTwentyFour').Value := varColumnTwentyFour;
    ExecSQL;
  end;

I get the error on this line:

Sql.Add('       :ColumnTwelve,                                    ');

which is the 11th parameter in my insert statement. If I comment this line I get the error in the next parameter. If I put the value directly like this:

Sql.Add('     ' + varColumnTwelve + ',                            ');

It works fine, but I get the error in the next parameter.

So it makes me wonder: does ADOQuery has a limit of how many parameters it can handle? Or if this isn't the real issue, does anyone has a clue of how I can fix this?


Notes:

  • I'm using Delphi 7 and Windows 8.1.

  • The AV only (and always) appears when debugging, it does never appear if I execute the application directly through its ".exe".

  • If I keep pressing "Run" after the error appears, it shows more and more AVs (I think that the number of AVs is the same as the number of parameteres that are added after the 10th), until the application continues running normally.

  • The insert works after all the AVs appeared on the screen. I just want to understand why am I getting this error when everything looks fine.

  • Are you using threads? – whosrdaddy May 18 '15 at 14:50
  • @whosrdaddy no, not using threads. – João Victor Oliveira May 18 '15 at 16:02
  • 1
    FYI there is a hardlimit of 1024 parameters on ADO, so that is clearly not the problem here. David is on the right track, the problem is somewhere else in your code, listen to his advice... – whosrdaddy May 18 '15 at 19:01
  • First thing to try is to wrap the query building in a `qry.SQL.BeginUpdate;`...`qry.SQL.EndUpdate` pair. I have (once, with Informix drivers IIRC) seen a similar problem. Without BeginUpdate..EndUpdate, TADOQuery will attempt to parse the parameters for each Add() – Gerry Coll May 18 '15 at 20:59
  • @Gerry - Begin/EndUpdate isn't involved in the parameter parsing resulting from a change in the SQL. That is achieved using **ParamCheck**. – Deltics May 19 '15 at 00:22
  • @Deltics - Sorry, memory fail - changing the SQL calls `TADOQuery.QueryChanged` via the FSql.OnChange event handler. This then calls `TCustomADODataSet.SetCommandText`, which in turn calls `TADOCommand.SetCommandText` -> `TADOCommand.AssignCommandText`. This has an local procedure `InitParams` which is called if the ParamCheck *property* is true. (This is as-of D2007) – Gerry Coll May 19 '15 at 04:21
  • Also, the error I encountered years ago wasn't related to parameters,but with the OLEDB driver layer borking when parsing the (incomplete) SQL for some reason. BeginUpdate/EndUpdate fixed that as the SQL was complete before CommandTExt was updated – Gerry Coll May 19 '15 at 04:25

3 Answers3

6

Changing the SQL property of a TADOQuery causes the TADOQuery to respond to that change, re-applying the modified SQL to the internal ADO component objects as well as re-parsing the SQL to identify any parameters.

For this reason, it is not advisable to modify SQL incrementally in this way. Quite apart from anything else, it is highly inefficient to have the SQL applied and parsed over and over again, before it is completely assembled.

In this case, by the time you get to the point of adding your 11th parameter, the SQL has been applied and parsed 28 times!

The fact that the AV that then results is occurring within SQLOLEDB.DLL suggests that whatever problem is occurring is a result of the changes to the SQL being applied to the internal ADO objects rather than in the VCL processing to identify parameters etc. As such, there is not much you are going to be able to do to fix the problem. The best you can do is avoid it.

You can eliminate some of this processing by setting ParamCheck := FALSE whilst modifying the SQL. This will prevent the VCL from attempting to re-parse the modified SQL to identify parameters. However, it will not prevent the SQL from being re-applied to the underlying ADO components in response to each change.

As a diagnostic exercise you could try setting ParamCheck := FALSE while modifying your SQL. When done, call the Parameters.Refresh method to ensure that the parameters collection is updated to reflect the finished SQL:

qryInsert.ParamCheck := FALSE;
qryInsert.SQL.Add(..);
qryInsert.SQL.Add(..);
qryInsert.SQL.Add(..);
qryInsert.SQL.Add(..);

qryInsert.Parameters.Refresh;

NOTE: With ParamCheck set to FALSE, you must call Parameters.Refresh before attempting to set any parameter values, otherwise the parameters won't yet exist in the Parameters collection!

If the AV still occurs after this change then this even more strongly indicates some issue with the internal ADO components not behaving well in response to repeated changes to the SQL, perhaps due to a failure to properly deal with incomplete (syntactically incorrect) SQL.

However, you can avoid triggering the change mechanism entirely by one of two ways.

Perhaps the simplest is to use BeginUpdate/EndUpdate on the TADOQuery SQL stringlist around your code that builds the SQL:

qryInsert.SQL.BeginUpdate;
try
  qryInsert.SQL.Add(..);
  qryInsert.SQL.Add(..);
  qryInsert.SQL.Add(..);

finally
  qryInsert.SQL.EndUpdate;
end;

This has the effect of suppressing the internal OnChange event inside the ADO query object until the call to EndUpdate, at which point the SQL will be applied to the internal ADO objects and the Parameters of the query object updated.

Alternatively you could assemble your SQL in an entirely separate string list, and then apply that to the TADOQuery SQL property as a single, direct change to the SQL.Text property:

sql := TStringList.Create;
try
  sql.Add(..);
  sql.Add(..);
  sql.Add(..);
  sql.Add(..);
  sql.Add(..);
  sql.Add(..);

  qryInsert.SQL.Text := sql.Text;

finally
  sql.Free;
end;

Either way, the result will be that the VCL will parse for parameters and the internal ADO objects will be updated only once, with a complete and (hopefully) syntactically correct SQL statement.

This second approach can involve a little less "boilerplate" - the try..finally here is purely to manage the temporary stringlist. If you were re-using an object in wider scope for this purpose, or using a SQL builder helper class which yields a simple string (as I do) then there is no need for this particular try..finally, making this a little more convenient and cleaner to apply:

SQLBuilder.Insert('MyTable');
SQLBuilder.AddColumn('ColumnOne');
SQLBuilder.AddColumn('ColumnTwo');

qryInsert.SQL.Text := SQLBuilder.SQL;

// qryInsert.SQL == INSERT INTO MyTable (ColumnOne, ColumnTwo)
//                  VALUES (:ColumnOne, :ColumnTwo)    

For example.

String vs TStringList

If your preferred technique for building the SQL yields a stringlist rather than a simple string, you may be tempted to assign the stringlist directly:

  qryInsert.SQL := sql;

But note that this performs an Assign() of the sql stringlist, effectively performing a 'deep copy'. You still need to ensure the stringlist assigned (sql in the above code) is freed appropriately.

Note also that this is also less efficient since it also copies other properties of the stringlist, including any objects associated with each string in the list. In this case where you are only interested in copying across the Text content of the stringlist there is no need to incur that (slight) and unnecessary overhead.

Deltics
  • 22,162
  • 2
  • 42
  • 70
  • Isn't `Query.Sql.BeginUpdate` better in this case? – EProgrammerNotFound May 19 '15 at 02:59
  • @EProgrammer - LOL - yep, quite right. I've gotten so use to using my SQLBuilder class (which yields a complete SQL statement for assignment to "SQL.Text" that I overlooked the simple use of Begin/EndUpdate in this case. I've updated the answer to highlight this alternative. Thanks – Deltics May 19 '15 at 03:25
  • @Deltics FWIW, I don't think that there really is a problem here beyond inconvenience whilst debugging. This is just an exception handled by the external library. – David Heffernan May 19 '15 at 07:21
  • With ParamCheck set to False, I got the "Parameter not found" error (and I called Parameters.Refresh before setting any values. I guess I would have to create the parameteres after that?). With Begin/EndUpdate I got the same problem I originally had, but just on the EndUpdate command. Still, +1 for showing a lot of alternatives. Thanks very much! – João Victor Oliveira May 19 '15 at 13:05
2

The AV only (and always) appears when debugging, it does never appear if I execute the application directly through its ".exe".

....

The insert works after all the AVs appeared on the screen. I just want to understand why am I getting this error when everything looks fine.

The access violation is raised in an external module, implemented in a language other than Delphi. Most likely the external code is behaving correctly and as designed, and the access violation is expected.

That may sound very odd but the external code clearly handles the exception since control does not pass to your code's exception handlers. As you observe, the program works correctly.

This is what is known as a first chance exception. The debugger is notified and breaks. But then control returns to the program and in this case the program deals with the exception and continues. And it is perfectly normal, albeit perhaps counter-intuitive, for code to raise a first chance access violation exception, but still be functioning correctly. As evidence for that claim, see this from an article written by a member of the VS development team:

Why the VS Debugger does not stop on first chance Access Violations (by default)?

....

The reason the default for first-chance AVs does not stop is that sometimes Windows calls will AV, and then catch the exception themselves and carry on happily. If we did default to stopping on first chance AVs we would stop users in some strange place in say kernel32.dll and many would be very confused.

So in terms of correctness, I don't think there's anything to worry about. But it does make debugging difficult. Try the various suggestions made by @Deltics. If by making those changes you happen to avoid the exception, that's all to the good. Otherwise you may need to, at least temporarily, suppress the debugger from breaking on exceptions.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • Is it really advisable to be quite so relaxed about an external exception when it is an *access violation* ? To me this has a nasty smell about it. – Deltics May 19 '15 at 08:13
  • @Deltics It does happen though. FastMM in debug mode is a prime example of code that produces first chance AVs as part of normal execution. And I think it's quite clear now that what I say is true. Clearly the exception is handled by the external code. – David Heffernan May 19 '15 at 08:26
  • FastMM is a memory manager. There is a *world* of difference between the exceptions from FastMM and an AV occurring inside an ADO provider. But i either case an AV is not like a "Division By Zero" etc. There is no case I can think where an AV can be correctly "handled" after the fact. The correct solution is to avoid getting them in the first place! Since we cannot change the code in SQLOLEDB to fix whatever bug there may be there, then the best we can and should do is determine what we are doing that is causing/contributing toward the AV and **stop doing it**. – Deltics May 19 '15 at 09:11
  • @Deltics I don't know why that external module is producing a first chance AV that it then handles. You don't know either. You think that it must indicate an actual problem, but you don't know that. There's no evidence yet to support your suspicion that there is an actual problem. Why would the external code have gone to the trouble of handling the exception? According to the asker, the program runs correctly. It is not, a priori, a defect if code produces an AV that is then handled. – David Heffernan May 19 '15 at 09:18
  • @Deltics One final point. What would you do if you tried all the tricks in your answer to no avail. The first chance exception was still raised and handled by the external library, but no erroneous behaviour could be discerned in the program. Would you give up and decide that there was a problem that could not be solved? – David Heffernan May 19 '15 at 09:32
  • 1
    Well I guess that answers my original question of "Why am I getting this AV". Now I will try to suppress the message with the "tricks" @Deltics suggested. Thanks guys! – João Victor Oliveira May 19 '15 at 13:08
  • No Joao - it doesn't answer why you are getting the AV. As David points out, we *cannot* know that without seeing the source of SQLOLEDB. But David is wrong to suggest that it might possibly be benign. There is simply no sensible situation in which an Access Violation (invalid access to protected memory) could be deliberate or meaningful as anything other than an unexpected failure in code. – Deltics May 19 '15 at 19:50
  • @David - more than once I have found code where a developer, unable to find the cause of an AV has given up and decided to simply wrap the problem in a **try..except { do nothing end;**. For all *you* know this is what is going on inside SQLOLEDB. Or, it may be that they are following a policy of not allowing exceptions to "escape" the provider. i.e. all exceptions are caught, which is NOT the same as saying that they are all *handled* in the sense of being *sensibly dealt with*. You are an experienced developer and I find it surprising to have to explain this sort of thing to you. – Deltics May 19 '15 at 19:53
  • @David - my final point to your final point. If having done everything possible to avoid the problem it is found that it still occurs then yes of course there is nothing to be done but live with it (and report the potential issue to the provider of the DLL involved). But you cannot jump straight to that scenario, and even doing so does not alter the thinking about the cause of the problem. Having to live with it does not mean that the developer of the offending code *intended* you to have to live with it. – Deltics May 19 '15 at 19:56
  • @Deltics It depends. It's possible that this exception is intentional and normal. I believe that you are mistaken in your belief that all such exceptions must be defects. The article from the VS developer contradicts your belief. Did you read that article yet? – David Heffernan May 19 '15 at 20:07
  • @João I'm not saying that for certain this first chance AV is expected. It may be a defect as Deltics believes. However, he is categorically wrong to make statements with absolute certainty that this indicates a defect. He simply doesn't know that. He is quite wrong to say that all first chance AVs are defects. The article I linked to is evidence enough. – David Heffernan May 19 '15 at 20:15
  • @Deltics Are you aware that this external library is a mainstream MS library. Do you really believe that they use Pokemon exception handling in such code? I think that their developers are a lot better than that. This is not low quality amateur software that we are talking about!! – David Heffernan May 19 '15 at 20:29
  • Just to give a feedback: I put the BeginUpdate/EndUpdate as Deltics suggested, and the AV now only appears at the EndUpdate. But as before the SQL works correctly and the program continues to work normally afterwards, and there isn't a single error message on the application, just on Delphi. So I accepted David explanation and moved on. – João Victor Oliveira May 21 '15 at 16:57
0

If qrySelect has no 'ColumnTwelve' then

if qrySelect.FieldByName('ColumnTwelve').AsSTring = 'Y' then

will raise an exception because the FieldByName will return nil

Keith Miller
  • 1,718
  • 1
  • 13
  • 22
  • 1
    But the question states that the AV occurs elsewhere, and does not always occur. If FieldByName returned nil then the error would occur always. – David Heffernan May 18 '15 at 14:34
  • 1
    Yes, that would raise exception, but `EDatabaseError`. – TLama May 18 '15 at 14:37
  • qrySelect is another TADOQuery that is ran before the qryInsert. That's not the problem. – João Victor Oliveira May 18 '15 at 15:55
  • @Keith - Yes it will raise an exception, but that exception will be an **EDatabaseError** (field not found), **not** an access violation. "Exception" and "access violation" are not synonymous. – Deltics May 19 '15 at 00:03