Analysis and repair of two BUGs in Delphi
When using Delphi 7 for three-tier database development, I encountered two small problems. Through repeated trials, I finally found out the two small BUGs in Delphi 7 and fixed them (it seems that there are the same BUGs in Delphi 6), writing This article shares the joy of success with everyone. I am also new to Delphi. There must be many things that are wrong in the article. I would like to ask friends to correct me.
BUG1. Chinese characters are truncated when passing parameters:
How to reproduce BUG:
SQL Server 2000 is used in the background, and there is an XsHeTong table for testing. You can adjust it according to your actual situation.
First create a data server: create a new project, create a remote data module, place one ADOConnection, ADODataSet, and DataSetPRovider on it, and make corresponding settings. Leave the ComamndText of ADODataSet blank, and set the poAllowCommandText in its Option to True. Compile and run.
Create a client program again: create a new project, place a DCOMConnection on the form, connect to the data server created previously, place a ClientDataSet, set its connection to the DCOMConnection here, and set its ProviderName to the server above The name of the DataSetProvider. Finally, place the DataSource and DBGrid and make corresponding settings to view the results, and then place a Button for testing.
Write code similar to the following in Button's OnClick (here I used the XsHeTong table and its two fields HTH (char 15), GCMC (varchar 100), you can adjust it according to your actual testing situation):
with ClientDataSet1 do
begin
Close;
CommandText := 'Insert Into XsHeTong(HTH, GCMC) values(:HTH,:GCMC)';
Params[0].AsString := '12345';
Params[1].AsString := 'Chinese characters that will be truncated';
Execute;
Close;
CommandText := 'Select * from XsHeTong';
Open;
end;
Run the program, click the button, and see that the record has been inserted. Unfortunately, the result is not correct. "Chinese characters that will be truncated" become "will be truncated", but "12345" without Chinese characters is inserted correctly.
BUG analysis and repair:
For comparison, I tried to directly use an ADOConnection, ADOCommand, and ADOTable to test the C/S architecture. The result was correct and the Chinese characters would not be cut off. This shows that this BUG only appears on the three-tier architecture.
Use SQL Server Profiler to probe the statements submitted to SQL Server for execution and find the following differences between the two-tier architecture and the three-tier architecture:
Two-tier architecture:
exec sp_executesql N'Insert into XsHeTong(HTH, GCMC) values(@P1,@P2)', N'@P1 varchar(15),@P2 varchar(100)', '12345', 'Chinese characters that will be truncated'
Three-tier architecture:
exec sp_executesql N'Insert into XsHeTong(HTH, GCMC) values(@P1,@P2)', N'@P1 varchar(5),@P2 varchar(7)', '12345', 'will be truncated
Obviously, in the two-layer architecture, the length of the parameter is passed according to the actual library structure. In the three-layer architecture, the length of the parameter is passed according to the string length of the actual parameter, and the actual string length seems to be miscalculated. A Chinese character is treated as two characters in length.
There is no choice but to trace and debug. In order to debug Delphi's VCL library, you need to select "Use Debug DCUs" in the "Compiler Options" of the project options.
First track the client program, then ClientDataSet1.Execute, and then go through a series of functions such as TCustomClientDataSet.Exectue, TCustomeClientDataSet.PackageParams, TCustomClientDataSet.DoExecute, etc., until AppServer.AS_Execute(ProviderName, CommandText, Params, OwnerData); submits the request to the server There are no abnormalities. It seems that the problem lies on the server side.
After tracking the server and repeated trials, I focused on the TCustomADODataSet.PSSetCommandText function. After repeated and detailed tracking, the target became more and more precise: TCustomADODataSet.PSSetParams, TParameter.Assign, TParameter.SetValue, VarDataSize. I finally found the source of the BUG: the VarDataSize function. Here is its code:
function VarDataSize(const Value: OleVariant): Integer;
begin
if VarIsNull(Value) then
Result := -1
else if VarIsArray(Value) then
Result := VarArrayHighBound(Value, 1) + 1
else if TVarData(Value).VType = varOleStr then
begin
Result := Length(PWideString(@TVarData(Value).VOleStr)^); //The problematic line
if Result = 0 then
Result := -1;
end
else
Result := SizeOf(OleVariant);
end;
It is in this function that the length of the actual parameter is calculated. It takes the address of the value in Value and uses it as a WideString pointer to find the length of the string. As a result, the string "Chinese characters that will be truncated" is The length becomes 7 instead of 14.
Once the problem is found, it is not difficult to solve it. Just simply
Result := Length(PWideString(@TVarData(Value).VOleStr)^); //The problematic line
Change to
Result := Length(PAnsiString(@TVarData(Value).VOleStr)^); //No problem
That's it.
But this will cause the length to be doubled when finding the length of the English string, so you can also change this line to:
Result := Length(Value);
In this way, the correct length can be obtained whether it is a Chinese, English or mixed Chinese-English string. This is a question that still puzzles me. Why does Borland go around in a circle to find the length of the parameter value through a pointer? If anyone knows, please explain it to me. Thank you very much!
Some friends may have questions, why does this problem of string truncation not occur when it is not done through a three-tier architecture? The answer is not complicated. When sending commands to SQL Server directly through ADOCommand, it determines the parameter length according to the table structure. It will first send a message to SQL Server
SET FMTONLY ON select HTH,GCMC from XsHeTong SET FMTONLY OFF
to get the table structure. Under the three-tier architecture, although TCustomADODataSet also uses the TADOCommand object internally to issue commands, it does not use this value as the parameter length after obtaining the table structure, but recalculates the length based on the actual parameters. The result This resulted in an error.
BUG2. Problem with the Lookup field of ClientDataSet:
Methods to reproduce BUG:
Create a new project and place two ClientDataSets on it, namely cds1 and cds2. Their data sources can be arbitrary. Among them, cds1 is the main data set. Add a new Lookup field in it. This Lookup field is based on a character field in cds1. value to find the corresponding value in cds2.
Generally speaking, it is normal to run the program, but once a single quote "'" appears in the value in the Lookup field of cds1 (you can modify or add a record, try entering a single quote), an error will occur immediately: Unterminated string constant.
BUG analysis and repair:
The cause of this BUG is much more obvious than the previous one. It must be caused by the failure to properly handle the side effects of single quotes.
Similarly, let’s track the source code of VCL:
Run the program, and when an error occurs, open the Call Stack window (in the View->Debug Windows) menu to check the function calls. Some of the previous calls are obvious and there is no problem. Let's start from the place related to Lookup to check the cause. The first one The function call related to Lookup is TField.CalcLookupValue. We set a breakpoint in this function, rerun the program, and perform single-step debugging after the interruption.
TCustomClientDataSet.Lookup->TCustomClientDataSet.LocateRecord
After several function calls above, we quickly set the target in the LocateRecord process. In this process, it generates corresponding filter conditions based on the settings of the Lookup field, and then adds the corresponding filter conditions to the target data set. The value is found, and the fault lies in the generation of filter conditions. For example, we need to go to cds2 based on the value of the Cust field (assumed to be 001) in cds1 and find the corresponding CustName field value based on the CustID field value. The generated condition should be [CustID] = '001', but if the value of Cust is aa'bb, the generated condition will become [CustID] = 'aa'bb', which obviously leads to an unfinished String constant.
Usually, when we solve the problem of single quotes appearing within single quotes, we only need to write two quotes in the quotes. The same is true here. As long as the generated condition becomes [CustID] = 'aa''bb', there will be no error. . So you can modify the source code like this:
Find the following code in the LocateRecord procedure:
ftString, ftFixedChar, ftWideString, ftGUID
if (i = Fields.Count - 1) and (loPartialKey in Options) then
ValStr := Format('''%s*''',[VarToStr(Value)]) else
ValStr := Format('''%s''',[VarToStr(Value)]);
Change to:
ftString, ftFixedChar, ftWideString, ftGUID:
if (i = Fields.Count - 1) and (loPartialKey in Options) then
ValStr := Format('''%s*''',[ StringReplace(VarToStr(Value),'''','''''',[rfReplaceAll])])
else
ValStr := Format('''%s''',[ StringReplace(VarToStr(Value),'''','''''',[rfReplaceAll])]);
That is, when generating the filter condition string, change all single quotes in the filter value of the condition from one to two.
In order to ensure the correctness of this modification, I checked the corresponding LocateRecord process in TCustomADODataSet (when using the Lookup field in TADODataSet, there will be no errors due to single quotes, only when using TCustomClientDataSet), its processing method is the same as TCustomClientDataSet is slightly different. It constructs filter conditions through the GetFilterStr function, but in GetFilterStr, it correctly handles the issue of single quotes. So looking at it this way, the problem of not correctly handling single quotes in LocateRecord of TCustomClientDataSet is indeed a minor omission by Borland.