I wrote my last post about enabling booleans in Firebird after several hours of poking around in database code trying to get my query to execute without errors. Once it worked, everything seemed great. But I missed an important step: I hadn’t tried to write anything back to the database yet.
Both Firebird DBX drivers blew up on me when I tried to do this, in different ways. The Embarcadero driver may or may not have had a problem with the booleans, but I never really got the chance to test that out because the object I was trying to write contained a blob field, and I got an exception from Firebird: “Incorrect values within sqlda structure.” A bit of Googling revealed that this is a known issue with writing blobs to Firebird that’s been in the Embarcadero driver for several years now. Apparently it’s still around. Not much I can do about that without the source code.
So I tried Chau Chee Yang’s driver. It didn’t have any trouble with the blobs, but it did choke on writing a Boolean field. I traced into the call into the database driver and looked at the assembly, and as near as I can tell, his implementation of the DBXWritableRow_SetBoolean function is:
result := 0;
Not hard to guess why. Firebird doesn’t support booleans, so no need to waste time writing a method for it. However, this is something I can fix without access to the driver source, with a little bit more RTTI surgery.
The standard version of the BOOLEAN domain is implemented as a special-case version of a smallint. So what we need is a way to write the value back out as a smallint instead of a boolean. When the driver DLL gets loaded by the DBExpress engine, it builds a method table object containing a bunch of function pointers to various functions in the DLL. One of them is the aforementioned DBXWritableRow_SetBoolean. There’s also a DBXWritableRow_SetInt16 that deals with writing out smallints. So a redirector function is easy enough to write:
function Passthrough_SetBoolean(Handle: TDBXWritableRowHandle;
Ordinal: TInt32; Value: LongBool): TDBXErrorCode; stdcall;
if value then
result := LSetSmallint(handle, ordinal, 1)
else result := LSetSmallint(handle, ordinal, 0);
(Yes, I need that if statement in there instead of simply calling Ord(value). As a LongBool, any nonzero value is considered True, and sometimes you’ll get things other than 1, for whatever reason.) LSetSmallint represents the current driver’s DBXWritableRow_SetInt16 function. Now you’ve just got to insert it into the method table. It’s buried under several inaccessible layers of encapsulation, of course, but the RTTI objects can dig it out.
procedure FixConnection(connection: TSqlConnection);
cls := ctx.GetType(connection.DBXConnection.ClassType);
driver := cls.GetField('FDriverDelegate').GetValue(connection.DBXConnection).AsType<TDBXDriver>;
cls := ctx.GetType(driver.classtype);
driver := cls.GetField('FDriver').GetValue(driver).AsType<TDBXDynalinkDriver>;
fld := ctx.GetType(TDBXDynalinkDriver).GetField('FMethodTable');
table := fld.GetValue(driver).AsType<TDBXMethodTable>;
table.FDBXWritableRow_SetBoolean := Passthrough_SetBoolean;
LSetSmallint := table.FDBXWritableRow_SetInt16;
This has to be run after you open the connection but before you try to write any booleans out.
BTW I emailed Chau Chee Yang about all this. He wrote back that he plans to support booleans in his driver “as soon as possible.” So let’s hope that he finds the time soon, and that Embarcadero is able to free up some of their limited development resources to fix up their driver issues. Tricks like this may be fun to dig around and build workarounds for, but in the end RTTI surgery is kind of an ugly hack that violates encapsulation, and it would be nice to not have to use it.