How to read a BLOB(Text) field using TpFIBQuery components from FibPlus - delphi

I am using Delphi 7 with the FibPlus components . One of them being TpFIBQuery.
I am loading data from a table with the generic
select * from TableName where Key = 1
One of the fields returned is of type BLOB(Text).
I can't seem to get the value into a string list informatie using either of the following 3 ways :
Informatie.Text := FieldByName('Informatie').AsString // Returns the string 'BLOB'
Informatie.Text := BlobAsString('Informatie') // Returns ''
BlobToStrings('Informatie',Informatie) // Returns ''
I have confirmed using Database Workbench that the field in the table indeed contains the saved text.
Anyone ?

usualy, i do like this
var
sl: TStrings; // blob IS NOT string!
ms: TMemoryStream;
begin
sl := TStringList.Create;
ms := TMemoryStream.Create;
try
q.FieldByName('x').SaveToStream(ms);
ms.Position := 0;
sl.LoadFromStream(ms);
// do what ever you want with sl here
// and here too
finally
sl.Free;
ms.Free;
end; // try..finally
end;
note that q is your TpFibQuery object.
also
select * from table is bad bad practice.
that habit eventually will lead you continuous headache.

After trying the solution of #jiang, which produced the same error, I finally have found the culprit.
Turns out it was an error due to my part ( it usually is, you just have to find it ).
Turns out I had set the read transaction to False sometime during the processing/reading of the fields of the original query.
I perform a lookup in another table to get the description of a integer value in the query.
This lookup query uses the same read transaction , and sets this transaction to False after looking up the description.
AFter returning to the original query, reading integer and string fields pose no problem (although the read transaction has been set to False), but reading a BLOB field into a string with the ...AsString method produces an error or returns 'BLOB'.
Obviously I need to set the read transaction to True at the start of the read actions and set it to False after ALL read transactions. This is the major change when you convert a Paradox BDE application to a Firebird of Sqlserver application.
Anyway, I am glad I have found the solution. Hopefully it will help others too.

Related

How to use a SQL where statement in delphi properly?

I have a problem with issuing a SQL statement. I know that the English value should be a string on its own and I've tried that but it keeps throwing me one of these errors
procedure TfrmPetersonGroup.btnEnglishClick(Sender: TObject);
var
sSqlQuery:string;
begin
//2.4
dmoBandB.qryQuery.SQL.Clear;
sSqlQuery:='DELETE FROM tblClients WHERE Nationality =' + ' English';
dmoBandB.qryQuery.SQL.Text := sSqlQuery;
dmoBandB.qryQuery.active := true;
end;
I suggest you get a safe query. As below:
procedure SafeDeleteReq(SQLQuery: TSQLQuery; del: string);
begin
SQLQuery.SQL.Text := 'DELETE FROM tblClients WHERE Nationality=:Nationality';
SQLQuery.ParamByName('Nationality').AsString := del;
SQLQuery.ExecSQL();
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
SafeDeleteReq(SQLQuery1, 'English');
end;
You could change your sSql to
sSqlQuery:='DELETE FROM tblClients WHERE Nationality = ' + QuotedStr('English');
just to get it working, but that's not the best idea, see below.
Your version of it caused the error because, without quotes around it, the Sql parser thought that English was an identifier, e.g. another column name like Nationality.
Using QuotedStr around column values ensures that single-quote characters embedded in the value, like
O'Brien
are escaped correctly.
The other thing is that you should replace
dmoBandB.qryQuery.active := true;
by
dmoBandB.qryQuery.ExecSql;
The reason is that setting Active to True is equivalent to calling .Open, which is invalid in this context because .Open only works if the Sql query returns a result set and DELETE does not (sorry, I should have noticed this problem first time around). Once you've called ExecSql, you can reopen the table by setting qryQuery's Sql.Text to a valid SELECT statement and then calling .Open.
However, a betteer way to avoid your initial problem would be to get into the habit of using parameterised Sql statements - see http://docwiki.embarcadero.com/RADStudio/Rio/en/Using_Parameters_in_Queries, which is applicable to all Sql DML statements (Insert, Delete, Update, Select, etc). Apart from anything else, this may help you avoid Sql Injection exploits (https://en.wikipedia.org/wiki/SQL_injection).

Can one create an MS Access table in Delphi (e.g., FireDAC) from the field defs of an existing table without using SQL?

I wanted to create a new mdb file containing tables based on the structure of previously existing tables. I knew that I can use newTable.FieldDefs.Add() to recreate the fields one by one in a loop. But since there is already an oldTable fully stocked with the correct FieldDefs, that seemed terribly inelegant. I was looking for a single-statement solution!
I had found that newTable.FieldDefs.Assign(oldTable.FieldDefs) would compile (and run) without error but it left newTable with zero defined fields. This caused me to erroneously conclude that I didn't understand that statement's function. (Later I found that it failed only when oldTable.open had not occurred, which could not happen when the database was not available, even though the FieldDefs had been made Persistent and were clearly visible in the Object Inspector)
Here is my original code after some sleuthing:
procedure TForm2.Button1Click(Sender: TObject);
var
fname: string;
Table: TFDTable;
FDConn: TFDConnection;
begin
fname := 'C:\ProgramData\mymdb.mdb';
if FileExists(fname) then DeleteFile(fname);
{ Make new file for Table }
FDMSAccessService1.Database := fname;
FDMSAccessService1.DBVersion := avAccess2000;
FDMSAccessService1.CreateDB;
{ Connect to new file }
FDConn := TFDConnection.Create(nil);
FDConn.Params.Database := fname;
FDConn.Params.DriverID := 'MSAcc';
FDConn.Connected := true;
{ Set up new Table using old table's structure }
Table := TFDTable.Create(nil);
try
{ ADOTable1 has been linked to an existing table in a prior
database with Field Defs made Persistent using the Fields
Editor in the Object Inspector. That database will not be
available in my actual use scenario }
try
ADOTable1.open; // Throws exception when database file not found
except
end;
Table.Connection := FDConn;
{ specify table name }
Table.TableName := ADOTable1.TableName;
Table.FieldDefs.Assign(ADOTable1.FieldDefs); // No errors reported
ShowMessageFmt('New Table %s has %d fields',[Table.TableName,
Table.FieldDefs.Count]);
{ Reports correct TableName but "0 fields" with table not open
(i.e. file not found). Reports "23 fields" with table open }
{ Set Table definition into new mdb file }
Table.CreateTable(False); // Throws exception when 0 fields found
finally
Table.Free;
end;
end;
It turned out that the solution was to use a ClientDataSet originally linked to the same old database instead of the ADOTable. See the working solution below in my answer.
Edit: A final note. I had hoped to use this FireDAC approach, as indicated here, to get around the lack of a TADOTable.CreateTable method. Alas, although the "solutions" above and below do work to create a new TADOTable, that table's field definitions are not faithful replicas of the original table. There may be a combination of the myriad TFDTable options that would get around this, but I was not able to discover it so I reverted to creating my ADO tables with SQL.
Thanks to #Ken White's (unfortunately deleted) pointer, I now think that I have a solution to my original question about cloning the field defs from an old table into a newly created database. My original problem stemmed from the fact that the FieldDefs function for a table evidently does not return the actual stored field data if the table is not "open" (i.e., connected to the relevant database). Since my use scenario would not have a valid database available I could not "open" the table. However, ClientDataSets have an additional option to "StoreDefs" along with editor options to "Fetch Params" and "Assign Local Data". With those settings saved, the ClientDataSet renders its FieldDefs properties without being "open". Using that approach it seems that I can clone the stored field defs to a new table without needing a currently valid database to read them from. Thanks again, Ken, you saved a lot of my remaining hair! I sure wish that Embarcadero would do a better job of rationalizing their help files. They removed BDE from the default installation of Rio while still pointing in their help file discussion on creating Access tables to its TTable type as the way to create new tables and then never point to the equivalent capabilities in FireDAC (or elsewhere) which they continue to support. I wasted a lot of time because of this "oversight"!
Here is my working code after Ken's tip:
procedure TForm1.Button1Click(Sender: TObject);
var
i: integer;
fname: string;
Table: TFDTable;
FDConn: TFDConnection;
begin
fname := 'C:\ProgramData\mymdb.mdb';
if FileExists(fname) then DeleteFile(fname);
FDMSAccessService1.Database := fname;
FDMSAccessService1.DBVersion := avAccess2000;
FDMSAccessService1.CreateDB;
FDConn := TFDConnection.Create(nil);
FDConn.Params.Database := fname;
FDConn.Params.DriverID := 'MSAcc';
FDConn.Connected := true;
Table := TFDTable.Create(nil);
try
Table.Connection := FDConn;
{ specify table name }
Table.TableName := 'ATable';
{ The existingClientDataSet has been linked to a table in the
prior, no longer valid, database using StoreDefs, Fetch Params,
and Assign Local Data in the Object Inspector }
Table.FieldDefs.Assign(existingClientDataSet.FieldDefs);
ShowMessageFmt('New Table has %d fields', [Table.FieldDefs.Count]);
Table.CreateTable(False);
finally
Table.Free;
end;

Error when using parameter in ADOQuery

I have this simple code to check if a record exists in a table, but it always returns a runtime error :
Arguments are of the wrong type, are out of acceptable range, or are
in conflict with one another.
my code is this :
function TDataModuleMain.BarCodeExists(barCode: string): boolean;
begin
if ADOQuerySql.Active then
ADOQuerySql.Close;
ADOQuerySql.SQL.Clear;
ADOQuerySql.SQL.Text := 'select count(1) from Card where BarCode = (:TestBarcode)';
ADOQuerySql.Parameters.ParamByName('TestBarcode').Value := barCode;
ADOQuerySql.Open; // HERE THE RUNTIME ERROR APPEARS
Result := ADOQuerySql.Fields[0].AsInteger = 1;
ADOQuerySql.Close;
ADOQuerySql.Parameters.Clear;
end;
The field BarCode in table Card is of type nvarchar(100)
In debug I see that the parameter is created, and gets populated with the correct value.
Running the query in sql server management studio also works.
I also found this How to pass string parameters to an TADOQuery? and checked my code with the code in the answer but I don't see any problems here.
Also this AdoQuery Error using parameters did not help me.
It will no doubt be something very simple that I have missed but I just dont see it now.
EDIT : things I tried from suggestions in the comments:
.ParamCheck := True (default)
.Parameters.ParamByName('TestBarcode').DataType := ftString
.Parameters.ParamByName('TestBarcode').DataType := ftWideString
None of these worked however.
What did help was using a non-shared AdoQuery for this, and that one did the job without any errors. I am using that now as the solution but I am still looking at the shared AdoQuery out of curiousity what the exact problem is.
EDIT: the source of the problem is found.
I used the function provided by MartinA to examine both the dynamic created query and the shared AdoQuery and I found one difference.
The shared AdoQuery had the this property filled :
ExecuteOption := [eoExecuteNoRecords]
and the dynamic created query does not.
Since this property is not set in designtime I did not see it.
After clearing the property to [] the shared AdoQuery worked again.
I am going to switch to using non shared AdoQuery for this kind of work as been suggested.
Thanks everyone for your assistance.
The following isn't intended to be a complete answer to your q, but to follow up my comment that "all you have to do is to inspect your form's DFM and compare the properties of your original ADoQuery with the unshared one. The answer should lie in the difference(s)" and your reply that the unshared query is created dynamically.
There is no "voodoo" involved in the difference in behaviour between your two ADOQuerys. It's just a question of capturing what the differences actually are.
So, what you need, to debug the problem yourself, is some code to compare the properties of two components, even if one or both of them is created dynamically. Using the following routine on both components will enable you to do exactly that:
function TForm1.ComponentToString(AComponent : TComponent) : String;
var
SS : TStringStream;
MS : TMemoryStream;
Writer : TWriter;
begin
// Note: There may be a more direct way of doing the following, without
// needing the intermediary TMemoryStream, MS
SS := TStringStream.Create('');
MS := TMemoryStream.Create;
Writer := TWriter.Create(MS, 4096);
try
Writer.Root := Self;
Writer.WriteSignature;
Writer.WriteComponent(AComponent);
Writer.FlushBuffer;
MS.Position := 0;
ObjectBinaryToText(MS, SS);
Result := SS.DataString;
finally
Writer.Free;
MS.Free;
SS.Free;
end;
end;
Over to you ...

Firebird TIBQuery insert with returning ... INTO

I have a firebird 2.x database with Generator and a trigger to generate the key field.
I need to get the returned value from below query.
INSERT INTO XXXX (vdate,description) values ('"+ VDate +"','"+ Description +"') returning vno INTO :ParamVoucherNo
I tried several versions of below code but it dont wrok and I get
Dynamic sql error sql error code = -104
Is it really possible to get the return value in delphi using TIBQuery ?
Query1->SQL->Clear();
Query1->SQL->Add("INSERT INTO XXXX (vodate,description) values ('"+ VDate +"','"+ Description +"') returning vno INTO :ParamVoucherNo");
Query1->Params->ParamByName("ParamVoucherno")->ParamType = ptResult;
Query1->Params->ParamByName("ParamVoucherno")->DataType = ftInteger;
Query1->Params->ParamByName("ParamVoucherno")->Value = "";
Query1->Prepare();
Query1->ExecSQL();
Any suggestions?
From Firebird README.returning:
The INTO part (i.e. the variable list) is allowed in PSQL only (to
assign local variables) and rejected in DSQL.
As IBX uses DSQL, you should exclude INTO part from your query.
INSERT ... RETURNING for DSQL looks the same as a call of a stored procedure, which returns result set. So, you have to use Open instead of ExecSQL.
Your mixing of dynamic SQL with parameters is just confusing.
Do this instead:
Query1->SQL->Clear();
Query1->SQL->Add("INSERT INTO table1 (vodate,description) VALUES"+
"(:VoDate,:Description) RETURNING vno INTO :VoucherNo ");
Query1->Params->ParamByName("VoDate")->Value = VDate;
Query1->Params->ParamByName("description")->Value = Description;
Query1->Prepare();
Query1->ExecSQL();
VoucherNo = Query1->Params->ParamByName("VoucherNo")->AsInteger;
Using Delphi 6 I have the ID returning successfully using an EXECUTE BLOCK statement:
EXECUTE BLOCK
RETURNS ( DeptKey INT )
AS
BEGIN
INSERT INTO DEPARTMENT
( COMPANY_KEY, DEPARTMENT_NAME )
VALUES ( 1, 'TEST1' ) RETURNING DEPARTMENT_KEY INTO :DeptKey;
SUSPEND;
END;
From Delphi you can do the folliowing:
FQuery.SQL.Text := '<Execute Block Statement>';
FQuery.Open();
ANewKey := FQuery.Fields[0].AsInteger;
IBX is not Firebird ready
you can take a look at FIBPLUS who support Firebird features
FIBPlus also supports FB2.0 insert ... into ... returning. Now you
should not bother about getting generator values from the client but
leave them in the trigger. You can also use RDB$DB_KEY. New possible
variants of work with insert returning and RDB$DB_KEY are shown in the
example “FB2InsertReturning”.
Why not get the next value for VoucherNo first, followed by
"INSERT INTO table1 (vno, vodate,description) VALUES (:VoucherNo,:VoDate,:Description)");
?
Your trigger can then either be dispensed with (which is nice), or modified to detect null (or <= zero can be useful too) and only then populate the vno field.
create trigger bi_mytable
active before insert position 1
on mytable
as
begin
if (new.vno is null)
then new.vno = next value for gen_VoucherNos;
end
Client-side you can :
select gen_id(gen_VoucherNos, 1) from rdb$database;
By modifying the trigger in this manner you save yourself a headache later on if/when you want to insert blocks of records
I wonder if that INSERT can be wrapped into EXECUTE BLOCK command.
Would IBX manage EXECUTE BLOCK then?
http://www.firebirdsql.org/refdocs/langrefupd20-execblock.html
http://firebirdsql.su/doku.php?id=execute_block
Hope to try it in both IBX and Unified Interbase in XE2
PS: Even if it does not, I found the library, that tells to work on top of IBX of Delphi XE2 (both x86 and x64) and to add EXECUTE BLOCK support: http://www.loginovprojects.ru/index.php?page=ibxfbutils#eb.
As I know there should be some changes to IBX made. Internally INSERT ... RETURNING should be treated the same way as a selectable procedure with returning parameters.
i know this question was answered a long time ago, but i must write this as clear as possible, for those who need this as i was.
i too, needed the "INSERT..RETURNING" thing.
the Delphi drove me crazy for a long time, until i changed my Data access components.
i even moved from Delphi XE2, to XE5 only because of that...
conclusion : IBX does NOT support RETURNING!
FireDAC is PERFECT for what i need with Firebird.
just move to FireDAC and you'll be able to do everything you need, and with high performance.
If you have a table with this 2 Fields: GRP_NO and GROUPNAME and you want to get the new GRP_NO you have to use RET_ as prefix, see example:
procedure TFormDatenbank.Button1Click(Sender: TObject);
var
q: Uni.TUniQuery;
ID: Integer;
GroupName: String;
begin
GroupName := 'MyGroupName';
q := TUniQuery.Create(nil);
try
q.Connection := Datenmodul.UniConnection;
q.ParamCheck := true; // the default value of ParamCheck is true.
q.SQL.Clear;
q.SQL.Add('SELECT GRP_NO, GROUPNAME FROM GROUPDATA WHERE GROUPNAME = :GROUPNAME');
q.ParamByName('GROUPNAME').AsString := GroupName;
q.Open;
if q.RecordCount > 0 then
ID := q.FieldByName('GRP_NO').AsInteger
else
begin
// there exist no group with this name, so insert this new name
q.SQL.Clear;
q.SQL.Add('INSERT INTO GROUPDATA');
q.SQL.Add('(GROUPNAME)');
q.SQL.Add('VALUES');
q.SQL.Add('(:GROUPNAME)');
q.SQL.Add('RETURNING GRP_NO;');
q.ParamByName('GROUPNAME').AsString := GroupName;
q.Execute;
ID := q.ParamByName('RET_GRP_NO').AsInteger;
end;
finally
q.Free;
end;
end;
From the IBx2 sources, you can do it like this:
//Uses IBSql;
//var Cur: IResults;
IBSQL1.SQL.Text := 'delete from tbl_document where id = 120 returning id;';
IBSQL1.Prepare;
if IBSQL1.Prepared then
begin
Cur := IBSQL1.Statement.Execute(IBTransaction1.TransactionIntf);
WriteLn(Cur.Data[cou].AsString);
Cur.GetTransaction.Commit(True);
end;
IResults interface Code:
IResults = interface
function getCount: integer;
function GetTransaction: ITransaction;
function ByName(Idx: String): ISQLData;
function getSQLData(index: integer): ISQLData;
procedure GetData(index: integer; var IsNull:boolean; var len: short; var data: PChar);
procedure SetRetainInterfaces(aValue: boolean);
property Data[index: integer]: ISQLData read getSQLData; default;
property Count: integer read getCount;
end;
Test enviroment:
Arch Linux X86
Firebird 3
Lazarus 1.9
FPC 3.0.4
Quick note: This works on new Firebird API in the IBX, But I didn't test it in Legacy Firebird API with the IBX.

I get invalid index descriptor when using ancient FoxPro 2.6 tables

I'm connecting to old FoxPro 2.6 tables in Delphi 2007.
I've installed the BDE and I put a TTable on the form.
One table doesn't work
Set the databasename to c:\datadir and
The tablename to contacts.dbf.
When I set active to true, I get
invalid index descriptor.
Another table works fine
I have another table called article.dbf that loads fine, and in the original program everything works fine as well.
Here's what I've tried
I've already re-indexed everything in the original program, but that makes no difference.
In fact the diagnostic from Foxpro says that everything is OK.
I don't really care about any indexes because there are not that many records in the table.
I've tried setting the indexfile property, but that doesn't help.
How do I get Delphi to just connect to the table and stop complaining about indexes?
Probably your contacts.dbf table contains expression index that cannot be evaluated by BDE. Here is an explanation I have found here
Question: What types of FoxPro indexes
are not supported by the BDE? When
trying to open some tables, I get an
"Invalid Index Descriptor" error.
Answer: This error occurs when the
production index (.CDX) associated the
table has an index tag which has an
expression which the BDE cannot
evaluate. The solution is to delete
the tag using FoxPro create an
eqivalent index that the BDE can
understand.
The following conditions are not
supported by the BDE and will cause
"Invalid Index Descriptor" error.
DTOC(, 1) format not supported; Use
DTOC(). ALLTRIM function not
supported; Use LTRIM(RTRIM(Field)).
Here's code that Sertac described that will remove the auto-open CDX flag from the header. Make a copy of the dataabase first, of course.
var
Stream: TFileStream;
ByteRead: Byte;
begin
Stream := TFileStream.Create("YourFile.dbf", fmOpenReadWrite or fmShareDenyNone);
try
// Byte offset 28 has a value of 0x01 if a structural (auto-open) index exists,
// or 0x00 if no such index exists. If the value is not set, we do nothing.
Stream.Position := 28;
Stream.Read(ByteRead, SizeOf(ByteRead));
if ByteRead = 1 then
begin
ByteRead := 0;
Stream.Position := 28;
Stream.Write(ByteRead, SizeOf(Byte));
end;
finally
Stream.Free;
end;
end;

Resources