Wrong field length when accessing Unicode DB in Ansi mode
Posted: Tue 20 Dec 2011 09:38
We do right now deliver the released version of our application with Ansi DB access and we want to switch to Unicode DB access somewhen next year (we do drive already an Unicode DB branch in parallel). Since it is necessary to have a DB with an Unicode charset like AL32UTF8 in order to store unicode strings, upgrading the DB charset isn't easy, our (not comprehensive) tests with the Ansi DB application against the Unicode DB where promising and so we planned to install Oracle using an Unicode charset for new customers to avoid the DB charset upgrade later. Lately we tried that, but using the application fails due wrong field lengths and field types in several modules.
I've created a test case and the reason for the failure seems to be in OraClasses.TOCICommand.GetFieldDesc8 where in mode CharUsed FConnection.FCharLength = 1 needs to be taken into account. Ironically this is done in the LITE version and so this would pass the test case. In order to fix the issue I've removed {$IFDEF LITE} in the following snippet
Changing > into >= in the last CharUsed if statement from
to
would also be an option.
The test case for this is the following:
SQL for the test table
Delphi console application
Steps:
- execute the script
- create a new console application in Delphi and paste the code
- adjust the constants cServer, cUserName and cPassword
(important DB parameters are NLS_LENGTH_SEMANTICS = Char and NLS_CHARACTERSET = AL32UTF8)
- compile and run the example
expected: output is PASS
actual:
FAIL #1 - 40
FAIL #2 - 10
FAIL #3 - TMemoField
FAIL #4 - ORA-12899: ..."ODACTESTUNICODEDB"."FIELD10" (...: 11, ...: 10)
FAIL
I've created a test case and the reason for the failure seems to be in OraClasses.TOCICommand.GetFieldDesc8 where in mode CharUsed FConnection.FCharLength = 1 needs to be taken into account. Ironically this is done in the LITE version and so this would pass the test case. In order to fix the issue I've removed {$IFDEF LITE} in the following snippet
Code: Select all
if CharUsed then
if FConnection.FUseUnicode {$IFDEF LITE}or (FConnection.FCharLength = 1){$ENDIF} then begin
Check(OCIAttrGet2(hParam, OCI_DTYPE_PARAM, ValueInt, nil, OCI_ATTR_CHAR_SIZE, hOCIError));
Field.Length := sb2(ValueInt);
end
Code: Select all
// in non Unicode mode and CharLength > 1 size of field = length in chars * size of char in bytes
else if FConnection.FCharLength > 1 then begin
Code: Select all
// in non Unicode mode and CharLength > 1 size of field = length in chars * size of char in bytes
else if FConnection.FCharLength >= 1 then begin
The test case for this is the following:
SQL for the test table
Code: Select all
CREATE TABLE ODACTESTUNICODEDB (
FIELD10 VARCHAR2(10),
FIELD240 VARCHAR2(240)
);
ALTER TABLE ODACTESTUNICODEDB ADD CONSTRAINT ODACTESTUNICODEDBPK PRIMARY KEY (FIELD10);
Code: Select all
program ODACAnsiAccessUnicodeDB;
{$APPTYPE CONSOLE}
uses
SysUtils,
Ora,
OraError,
OraSmart;
{
CREATE TABLE ODACTESTUNICODEDB (
FIELD10 VARCHAR2(10),
FIELD240 VARCHAR2(240)
);
ALTER TABLE ODACTESTUNICODEDB ADD CONSTRAINT ODACTESTUNICODEDBPK PRIMARY KEY (FIELD10);
}
const
cServer = 'YourServer';
cUsername = 'YourUser';
cPassword = 'YourPassword';
function TestAnsiAccessUnicodeDBFieldLength: Boolean;
var
se: TOraSession;
qr: TSmartQuery;
FS, Counter: Integer;
begin
Counter := 0;
se := TOraSession.Create(nil);
qr := TSmartQuery.Create(nil);
try
se.Server := cServer;
se.Username := cUserName;
se.Password := cPassword;
//se.Options.UseUnicode := True;//#1 makes it pass
//se.Options.Charset := 'WE8MSWIN1252';//#2 does not help
with qr do
begin
Session := se;
Options.LongStrings := False;//#3 fails also with True (False demos wrong field type)
SQL.Add('DELETE FROM ODACTESTUNICODEDB');
ExecSQL;
SQL.Clear;
SQL.Add('SELECT * FROM ODACTESTUNICODEDB');
Keyfields := 'FIELD10';
Open;
//check VARCHAR2(10) displaywidth
FS := Fields[0].DisplayWidth;
if FS = 10 then
Inc(Counter)
else
WriteLn('FAIL #1 - ', FS);
//check VARCHAR2(240) displaywidth
FS := Fields[1].DisplayWidth;
if FS = 240 then
Inc(Counter)
else
WriteLn('FAIL #2 - ', FS);
//with LongStrings = False VARCHAR2(240) should be TxStringField
if Pos('StringField', Fields[1].ClassName) > 0 then
Inc(Counter)
else
WriteLn('FAIL #3 - ', Fields[1].ClassName);
//insert a record with max number of chars
Insert;
Fields[0].AsString := '0123456789';
Post;
//insert a record with number of chars > max number of chars
//o leads to "ORA-12899: ... (actual: 11, maximum: 10)" in failure case
//o should lead to "ORA-00001..." in pass case, because content gets truncated after 10th char
try
Insert;
Fields[0].AsString := '0123456789+';
Post;
except
on E: EOraError do
begin
if EOraError(E).ErrorCode = 1 then
Inc(Counter)
else
if (EOraError(E).ErrorCode = 12899) and (Pos(': 10', E.Message) > 0) then
WriteLn('FAIL #4 - ' + E.Message)
else
raise;
end
else
raise;
end;
Close;
end;
finally
se.Free;
end;
Result := Counter = 4;
end;
begin
try
if TestAnsiAccessUnicodeDBFieldLength then
WriteLn('PASS')
else
WriteLn('FAIL');
except
on E: Exception do
begin
WriteLn('FAIL - Exception Error');
WriteLn(' E.ClassName = ', E.ClassName);
WriteLn(' E.Message = ', E.Message);
end;
end;
ReadLn;
end.
- execute the script
- create a new console application in Delphi and paste the code
- adjust the constants cServer, cUserName and cPassword
(important DB parameters are NLS_LENGTH_SEMANTICS = Char and NLS_CHARACTERSET = AL32UTF8)
- compile and run the example
expected: output is PASS
actual:
FAIL #1 - 40
FAIL #2 - 10
FAIL #3 - TMemoField
FAIL #4 - ORA-12899: ..."ODACTESTUNICODEDB"."FIELD10" (...: 11, ...: 10)
FAIL