How can I implement "writable calculated" fields in a TDataSet? - delphi

I am in the need to add additional fields to a TDataSet that don't exist in the underlying database but can be derived from existing fields. I can easily do this with caclulated fields and that works perfectly.
Now I want to edit these fields and write the changed data back. I can reverse the calculation to write the data back into the existing fields, but the DB controls just don't let me edit calculated fields.
Is there any approach that allows me to do this?
Update:
Ok, some more details about the background.
The dataset has a blob field, which is a TBytes representation. Some of the bytes are identified to contain information that can be represented in a convenient way with existing DB edit fields. Not all of the bytes are known, though, so the TBytes representation has to be kept as it is for processing through another application that knows about it. This app also modifies existing and inserts new records.
The TBytes of different records in the dataset often map to different fields representations, although setting a filter or range on the dataset will ensure that they have the same mapping.
As I said, extracting the known bytes and convert it into strings, dates, numbers and so on via calculated fields is no problem. Reconverting those values into the TBytes is also possible. The problem is making those extra fields editable, while keeping the dataset navigation intact.
If it helps: We have classes that do the bidirectional mapping, exposing the fields as published properties.

The answer depends on a data access components you are using. I am using Anydac and it support fkInternalCalc fields, which may be as calculated as manually edited.

I think calculated fields are by definition read-only, with values calculated on the client.
What you want could probably be implemented by an updatable view. You could define the view with calculated fields already - these would be calculated in SQL on the server - and an update trigger, maybe an insert trigger, too - to perform the reverse calculation. Then from the client you could use the view transparently like a table.

I had similar Issue with a ClientDataSet, i have solved this issue using dummy fileds on the SQL-Stmt so i could simulate the Fields in the Database.
See my Question

You can use TDatasetProvider.OnGetRecords (doesn't remember if this is the correct name of the event) and modify the datapacket sent to the Clientdataset.
Of course, you'll have to deal with them on an ApplyUpdates handler, as TOndrej said.

Woll2Woll's infopower components (I just tested their TwwDBEdit) allow to do such thing. So I would think whatever blocks you is at the TDBEdit level (or at the TFieldDataLink level).
What exactly differs in TwwDBEdit, I don't know. (And I'm not sure the license agreement would allow me to post here...).

In our database design, some values are percentages relative to another column (called oMean below), whereas other float values are stored as absolutes. Our customers later wanted both options (rel. and abs.) for all fields, so we came up with the following class derived from TFloatField. It should work for all TDataSet descendants.
unit EditableCalcFloatField;
interface
uses
db, classes;
type
TEditableCalcFloatField = class(TFloatField)
public
oAbs, oRel, oMean: TField;
protected
function GetCanModify: Boolean; override;
procedure SetAsFloat(Value: Double); override;
end;
implementation
function TEditableCalcFloatField.GetCanModify: Boolean;
begin
Result := oMean.AsFloat <> 0;
if not Result then Exit;
Result := (oAbs <> nil) or (oRel <> nil);
end;
procedure TEditableCalcFloatField.SetAsFloat(Value: Double);
var
fMean : Double;
begin
inherited;
if DataSet.State in [dsEdit, dsInsert] then begin
fMean := oMean.AsFloat;
if fMean = 0 then Exit;
if oAbs <> nil then
oAbs.AsFloat := Value / 100 * fMean
else
oRel.AsFloat := Value / fMean * 100;
end;
end;
end.
To use it without a package, you have to create the field in FormCreate, before the dataset is opened:
with TEditableCalcFloatField.Create(Self) do
begin
oAbs := sqlMerkmaleYourAbsoluteColumn;
DisplayLabel := sDisp;
oMean := sqlMerkmaleAssignedValue_Mean;
Calculated := True;
FieldName := 'R' + oAbs.FieldName;
DataSet := sqlMerkmale;
end;
And of course, its contents can be set either in the OnCalcFields event or by the user.

Use a TQuery descendant (MyQuery) with 'Select *, 0 as TempField from MyTable'
Procedure MyQueryAfterOpen(Dataset:TDataSet);
Begin
DataSet.FieldByName('TempField').ReadOnly := False;
End;
It is now an editabe temporary field

Related

How to search a FMX.TListView header as well as items

I have a LiveBindings databound FMX.TListView with the FieldName being Stage and the FillHeaderFieldName being Production. When the app is running, I see a list of Productions using the HeaderAppearance, and within each Production, there is a list of Stages using the ItemAppearance. I've turned on SearchVisible to get the components search panel to show at the top of the list.
Currently, typing into the search box only filters on the Stage, and not the Production.
I'd like to be able to do both, and I'd like to be able to do it without making another REST call with filter parameters. I understand I would probably need to write an event handler for the OnSearchChange event, and I have this bit of code to get the search text entered:
List := Sender as TListView;
for I := 0 to List.Controls.Count-1 do
if List.Controls[I].ClassType = TSearchBox then
begin
SearchBox := TSearchBox(List.Controls[I]);
break;
end;
And I think I need to set the Items.Filter property, and I used this bit of code:
Lower := LowerCase(SearchBox.Text.Trim);
List.Items.Filter :=
function(X: string): Boolean
begin
Result:= (Lower = EmptyStr) or LowerCase(X).Contains(Lower);
end;
One of the problems is that the ListView component is applying its filtering as soon as a character is typed, while the OnSearchChange event only fires when the searchbox loses focus.
The second problem is that, even after the event is fired and the new filter function set, nothing happens to the list.
I've confirmed that the List.Items collection in my "36" example does actually contain all 6 items - the 3 header items and the 3 detail items - so I'm unsure why the filter is not applying to the header items as it does the detail items.
I tried this out and found a solution. Keep in mind I don't have access to Delphi 10.3 Rio. I'm using 10.1 Berlin. Also keep in mind that what I usually do is bind in the code and not visually. But for this I stuck to visual binding.
As a dataset I have used a TFDMemoryTable (mt1) with 2 data fields (fmt1Prod and fmt1Stage) and 1 calculated field (fmt1Search). I have the following handler to calculate the Search field:
Procedure TForm2.mt1CalcFields(DataSet: TDataSet);
Begin
fmt1Search.AsString := fmt1Prod.AsString + '|' + fmt1Stage.AsString;
End;
I put some random data in the memory table OnFormCreate:
Procedure TForm2.FormCreate(Sender: TObject);
Var
i1, i2: Integer;
s1, s2: String;
Begin
mt1.CreateDataSet;
For i1 := 1 To 13 Do Begin
s1 := 'Prod' + FormatFloat('00', i1);
For i2 := Random(6) To Random(14) Do Begin
s2 := 'Stage' + FormatFloat('00', i2);
mt1.Append;
fmt1Prod.AsString := s1;
fmt1Stage.AsString := s2;
mt1.Post;
End;
End;
End;
I have put on Form2 a TGrid and a TListView. Both are bound to the dataset. Data and calculated fields show up properly in the TGrid (just to check).
The TListView is bound to the dataset as follows:
Synch <-> *
ItemHeader.Text <- Prod
ItemHeader.Break <- Prod
Item.Text <- Search
Item.Detail <- Stage
I did this because I cannot find a way to have the TListView searchbox work on anything but the Text of the items. Ok then... but this can be worked around though:
Set TListView.ItemAppeance to Custom
Find the TListView/ItemAppearance/Item/Text object in the structure and set Visible to False
Find the TListView/ItemAppearance/Item/Detail object in the structure and set Visible to True
I'm not sure all of the above is necessary, but it works. If your TListView is editable then you will probably need to fiddle with the ItemEditAppearance too.
Remember that with custom item appearance you can actually set the list view items to look just about anyway you want. You can add and remove labels, images and other things. It's not as powerful as designing a form, but you can do a lot with it. But all you really need here is to hide the search text and show the stage text somewhere in the item.
And... for more sophisticated item appearance you may have to do some code binding (non sure of this though).
If use bind visually and ItemAppearance (Dynamic Appearance) you can one column from data source assign to header and text item (visible = false). In this situation in header and item we have the same value and search work fine.

How to fix 'Unable to find record. No key specified'?

I'am using a firebird 2.5 server to write in a Database file(BD.fbd). My delphi XE8 project has a Data module(DMDados) with:
SQLConnection (conexao)
TSQLQUery1 (QueryBDPortico_Inicial) + TDataSetProvider1 (DSP_BDPortico_Inicial) + TClientDataSet1 (cdsBDPortico_Inicial)
TSQLQUery2 (QueryConsulta) (just for use SQL strings)
My database file has this table:
PORTICO_INICIAL
The table has these fields (all integer):
NPORTICO
ELEMENTO
ID
None of those fields are primary keys because I will have repeated values in some cases. The connection with the file is ok. The client data set is open when I run the code. The TSQLQUery2 (QueryConsulta) is open when needed.
My code, when triggered by a button, has to delete all tables' records (if exist) then full the table with integer numbers created by a LOOP.
In the first try the code just work fine, but when I press the button the second time i get the error 'Unable to find record. No key specified' then when I check the records the table is empty.
I tried to change the ProviderFlags of my query but this make no difference. I checked the field names, the table name or some SQL text error but find nothing.
My suspect is that when my code delete the records the old values stay in memory then when try apply updates with the new values the database use the old values to find the new record's place therefore causing this error.
procedure monta_portico ();
var
I,K,L,M, : integer;
begin
with DMDados do
begin
QUeryCOnsulta.SQL.Text := 'DELETE FROM PORTICO_INICIAL;';
QueryConsulta.ExecSQL();
K := 1;
for I := 1 to 10 do
begin
L := I*100;
for M := 1 to 3 do
begin
cdsBDPortico_Inicial.Insert;
cdsBDPortico_Inicial.FieldbyName('NPORTICO').AsInteger :=
M+L;
cdsBDPortico_Inicial.FieldbyName('ELEMENTO').AsInteger := M;
cdsBDPortico_Inicial.ApplyUpdates(0);
K := K +1;
end;
end;
end;
end;
I want that every time I use the code above it first delete all records in the table then fill it again with the loop.
When I use the code for the first time it do what I want but in the second time it just delete the records and can not fill the table with the values.
Update I've added some example code below. Also, when I wrote the original version of this answer, I'd forgotten that one of the TDataSetProvider Options is
poAllowMultiRecordUpdates, but I'm not sure that's involved in your problem.
The error message Unable to find record. No key specified is generated by the DataSetProvider, so isn't directly connected to your
QUeryCOnsulta.SQL.Text := 'DELETE FROM PORTICO_INICIAL;'
because that bypasses the DataSetProvider. The error is coming from an failed attempt to ApplyUpdates on the CDS. Try changing your call to it to
Assert(cdsBDPortico_Inicial.ApplyUpdates(0) = 0);
That will show you when the error occurs because the return result of ApplyUpdates gives the number of errors that occurred when calling it.
You say
will have repeated values in some cases
If that's true when the problem occurs, it's because you are hitting a fundamental limitation in the way a DataSetProvider works. To apply the updates on the source dataset, it has to generate SQL to send back to the source dataset (TSqlQuery1) which uniquely identifies the row to update in the source data, which is impossible if the source dataset contains duplicated rows.
Basically, you need to re-think your code so that the source dataset rows are all unique. Once you've done that, setting the DSP's UpdateMode to upWhereAll should avoid the problem. It would be best for the source dataset to have a primary key, of course.
A quick work-around would be to use CDS.Locate in the loop where you insert the records, to see if it can locate an already-existing record with the values you're about to add.
Btw, sorry for raising the point about the ProviderFlags. It's irrelevant if there are duplicated rows, because whatever they are set to, the DSP will still fail to update a single record.
In case it helps, here is some code which might help populating your table
in a way which avoids getting duplicates. It only populates the first two
columns, as in the code you show in your q.
function RowExists(ADataset : TDataSet; FieldNames : String; Values : Variant) : Boolean;
begin
Result := ADataSet.Locate(FieldNames, Values, []);
end;
procedure TForm1.PopulateTable;
var
Int1,
Int2,
Int3 : Integer;
i : Integer;
RowData : Variant;
begin
CDS1.IndexFieldNames := 'Int1;Int2';
for i := 1 to 100 do begin
Int1 := Round(Random(100));
Int2 := Round(Random(100));
RowData := VarArrayOf([Int1, Int2]);
if not RowExists(CDS1, 'Int1;Int2', RowData) then
CDS1.InsertRecord([Int1, Int2]);
end;
CDS1.First;
Assert(CDS1.ApplyUpdates(0) = 0);
end;
Splite the problem into small parties using functions and procedures
create an instance of TSqlQuery Execute the SQL statment's and destroy the instance when you finish with it...
procedure DeleteAll;
var
Qry: TSqlQuery;
begin
Qry := TSqlQuery.Create(nil);
try
Qry.SqlConnection := DMDados.conexao;
Qry.Sql.Text := 'DELETE FROM PORTICO_INICIAL;';
Qry.ExecSql;
finally
Qry.Free;
end;
end;
your can even execute directly from TSQlConnection with one line...
DMDados.conexao.ExecuteDirect('DELETE FROM PORTICO_INICIAL;')
procedure monta_portico ();
var
I,K,L,M, : integer;
begin
with DMDados do
begin
DeleteAll;
K := 1;
for I := 1 to 10 do
begin
L := I*100;
for M := 1 to 3 do
begin
cdsBDPortico_Inicial.Insert;
cdsBDPortico_Inicial.FieldbyName('NPORTICO').AsInteger :=
M+L;
cdsBDPortico_Inicial.FieldbyName('ELEMENTO').AsInteger := M;
cdsBDPortico_Inicial.ApplyUpdates(0);
K := K +1;
end;
end;
end;
end;
Just few obvervations, cause the primary answers were given, but they not deal with the secondary problems.
cdsBDPortico_Inicial.FieldbyName('NPORTICO').AsInteger :=
FieldByName is slow function - it is linear search over objects array with uppercased string comparison over each one. You better only call it once for every field, not do it again in again in the loop.
cdsBDPortico_Inicial.ApplyUpdates(0);
Again, applying updates is relatively slow - it requires roundtrip to the server all through internal guts of DataSnap library, why so often?
BTW, you delete rows from SQL table - but where do you delete rows from cdsBDPortico_Inicial ??? I do not see that code.
Was I in your shows I would write something like that (granted I am not big fan of Datasnap and CDS):
procedure monta_portico ();
var
Qry: TSqlQuery;
_p_EL, _p_NP: TParam;
Tra: TDBXTransaction;
var
I,K,L,M, : integer;
begin
Tra := nil;
Qry := TSqlQuery.Create(DMDados.conexao); // this way the query would have owner
try // thus even if I screw and forget to free it - someone eventually would
Qry.SqlConnection := DMDados.conexao;
Tra := Qry.SqlConnection.BeginTransaction;
// think about making a special function that would create query
// and set some its properties - like connection, transaction, preparation, etc
// so you would not repeat yourself again and again, risking mistyping
Qry.Sql.Text := 'DELETE FROM PORTICO_INICIAL'; // you do not need ';' for one statement, it is not script, not a PSQL block here
Qry.ExecSql;
Qry.Sql.Text := 'INSERT INTO PORTICO_INICIAL(NPORTICO,ELEMENTO) '
+ 'VALUES (:NP,:EL)';
Qry.Prepared := True;
_p_EL := Qry.ParamByName('EL'); // cache objects, do not repeat linear searches
_p_NP := Qry.ParamByName('NP'); // for simple queries you can even do ... := Qry.Params[0]
K := 1;
for I := 1 to 10 do
begin
L := I*100;
for M := 1 to 3 do
begin
_p_NP.AsInteger := M+L;
_p_EL.AsInteger := M;
Qry.ExecSQL;
Inc(K); // why? you seem to never use it
end;
end;
Qry.SqlConnection.CommitFreeAndNil(tra);
finally
if nil <> tra then Qry.SqlConnection.RollbackFreeAndNil(tra);
Qry.Destroy;
end;
end;
This procedure does not populate cdsBDPortico_Inicial - but do you really need it?
If you do - maybe you can re-read it from the database: there could be other programs that added rows into the table too.
Or you can insert many rows and then apply them all in one command, before committing the transaction (often abreviated tx), but even then, do not call FieldByName more than once.
Also, think about logical blocks of your program work in advance, those very transactions, temporary TSQLQuery objects etc.
However boring and tedious it is now, you would bring yourself many more spaghetti trouble if you don't. Adding this logic retroactively after you have many small functions calling one another in unpredictable order is very hard.
Also, if you make Firebird server auto-assigning the ID field (and your program does not need any special values in ID and will be ok with Firebird-made values) then the following command might server yet better for you: INSERT INTO PORTICO_INICIAL(NPORTICO,ELEMENTO) VALUES (:NP,:EL) RETURNING ID

Loading millions of records into a stringlist can be very slow

how can i load millions of records from tadotable into a stringlist very fast?
procedure TForm1.SlowLoadingIntoStringList(StringList: TStringList);
begin
StringList.Clear;
with SourceTable do
begin
Open;
DisableControls;
try
while not EOF do
begin
StringList.Add(FieldByName('OriginalData').AsString);
Next;
end;
finally
EnableControls;
Close;
end;
end;
in your loop you get the field.
Search the field out of the loop
procedure TForm1.SlowLoadingIntoStringList(StringList: TStringList);
var
oField: TField;
begin
StringList.Clear;
with SourceTable do
begin
Open;
DisableControls;
try
oField:= FieldByName('OriginalData');
if oField<>Nil then
begin
while not EOF do
begin
StringList.Add(oField.AsString);
Next;
end;
end;
finally
EnableControls;
Close;
end;
end;
end;
Unfortunately, you can't do this quickly. It is an inherently slow operation that involves large amounts of CPU time and memory bandwidth to achieve. You could throw more hardware at it, but I suspect you should be re-thinking your task instead.
With 'millions of records' you may consider :
1/ Change your Query from
SELECT * FROM MYTABLE;
in
SELECT OriginalData FROM MYTABLE;
You'll use less memory and be more efficient.
2/ Look another component than TStringList depending on your needs.
3/ Look all good previous advices, mainly :
don't use FieldByName
direct link to the OleDB provider
Is it sorted?
// Turn off the sort for now
StringList.Sorted := False;
// Preallocate the space
StringList.Capacity := recordCount;
// Now add the data with Append()
...
// Now turn the sort back on
StringList.Sorted := True;
Seriously? Millions of records in a stringlist?
Ok, let's assume you really do need to take this approach...
There are some good suggestions already posted.
If you want to experiment with a different approach you could consider concatenating the individual records server side (via a stored procedure) and then returning the concatenated data as a blob (or possibly nvarchar(max)), which is basically the list of concatenated strings delimited by say a carriage return (assuming this is a reasonable delimiter for your needs).
You can then simply assign the returned value to the Text property of the TStringList.
Even if you cannot do all of the strings in a single hit, you could do them in groups of say 1000 at a time.
This should save you a ton of time looping around each record client side.
Expanding on #Ravaut123's answer I would suggest the following code:
Make sure your Query is not connected to any visual other component, and does not have any events set that fire on rowchanges because this will cause it to to updates on every change in the active record, slowing things way down.
You can disable the visual controls using disablecontrols, but not the events and non-visual controls.
...
SQLatable:= 'SELECT SingleField FROM atable ORDER BY indexedfield ASC';
AQuery:= TAdoQuery.Create(Form1);
AQuery.Connection:= ....
AQuery.SQL.Text:= SQLatable;
Using a Query makes sure you only select 1 field, in the order that you want, this reduces network traffic. A table fetches all fields, causing much more overhead.
function TForm1.LoadingAllIntoStringList(AQuery: TAdoQuery): TStringList;
var
Field1: TField;
begin
Result:= nil;
try
if not(AQuery.Active) then begin
AQuery.Open;
end else begin
AQuery.First;
end;
AQuery.DisableControls;
AQuery.Filtered:= false; //Filter in the SQL `where` clause
AQuery.FetchAll; //Preload all data into memory
Result:= TStringlist.Create;
except
{ignore error, will return nil}
end;
try
Result.Sorted:= false; //Make sure you don't enable sorting
Result.Capacity:= AQuery.RecordCount; //Preallocate the needed space
Field1:= AQuery.FieldByName('SingleField'); //Never use `fieldbyname` in a loop!
while not AQuery.EOF do begin
Result.Add(Field1.AsString);
AQuery.Next;
end; {while}
AQuery.EnableControls;
except
FreeAndNil(Result);
end;
If you want to load the data into the stringlist to do some processing, consider doing that in the SQL statement instead. The DB can use indexes and other optimizations that the stringlist cannot use.
If you want to save that data into a CSV file, consider using a build-in DB function for that.
e.g. MySQL has:
SELECT X FROM table1 INTO OUTFILE 'c:/filename_of_csv_file.txt'
Which will create a CSV file for you.
Many DB's have simular functions.

How can I search faster for name/value pairs in a Delphi TStringList?

I implemented language translation in an application by putting all strings at runtime in a TStringList with:
procedure PopulateStringList;
begin
EnglishStringList.Append('CAN_T_FIND_FILE=It is not possible to find the file');
EnglishStringList.Append('DUMMY=Just a dummy record');
// total of 2000 record appended in the same way
EnglishStringList.Sorted := True; // Updated comment: this is USELESS!
end;
Then I get the translation using:
function GetTranslation(ResStr:String):String;
var
iIndex : Integer;
begin
iIndex := -1;
iIndex := EnglishStringList.IndexOfName(ResStr);
if iIndex >= 0 then
Result := EnglishStringList.ValueFromIndex[iIndex] else
Result := ResStr + ' (Translation N/A)';
end;
Anyway with this approach it takes about 30 microseconds to locate a record, is there a better way to achieve the same result?
UPDATE: For future reference I write here the new implementation that uses TDictionary as suggested (works with Delphi 2009 and newer):
procedure PopulateStringList;
begin
EnglishDictionary := TDictionary<String, String>.Create;
EnglishDictionary.Add('CAN_T_FIND_FILE','It is not possible to find the file');
EnglishDictionary.Add('DUMMY','Just a dummy record');
// total of 2000 record appended in the same way
end;
function GetTranslation(ResStr:String):String;
var
ValueFound: Boolean;
begin
ValueFound:= EnglishDictionary.TryGetValue(ResStr, Result);
if not ValueFound then Result := Result + '(Trans N/A)';
end;
The new GetTranslation function performs 1000 times faster (on my 2000 sample records) then the first version.
THashedStringList should be better, I think.
In Delphi 2009 or later I would use TDictionary< string,string > in Generics.Collections.
Also note that there are free tools such as http://dxgettext.po.dk/ for translating applications.
If THashedStringList works for you, that's great. Its biggest weakness is that every time you change the contents of the list, the Hash table is rebuilt. So it will work for you as long as your list remains small or doesn't change very often.
For more info on this, see: THashedStringList weakness, which gives a few alternatives.
If you have a big list that may be updated, you might want to try GpStringHash by gabr, that doesn't have to recompute the whole table at every change.
I think that you don't use the EnglishStringList(TStringList) correctly. This is a sorted list, you add elements (strings), you sort it, but when you search, you do this by a partial string (only the name, with IndexOfName).
If you use IndexOfName in a sorted list, the TStringList can't use Dicotomic search. It use sequential search.
(this is the implementation of IndexOfName)
for Result := 0 to GetCount - 1 do
begin
S := Get(Result);
P := AnsiPos('=', S);
if (P <> 0) and (CompareStrings(Copy(S, 1, P - 1), Name) = 0) then Exit;
end;
I think that this is the reason of poor performance.
The alternative is use 2 TStringList:
* The first (sorted) only containts the "Name" and a pointer to the second list that contain the value; You can implement this pointer to the second list using the "pointer" of Object property.
* The second (not sorted) list containt the values.
When you search, you do it at first list; In this case you can use the Find method. when you find the name, the pointer (implemented with Object property) give you the position on second list with the value.
In this case, Find method on Sorted List is more efficient that HashList (that must execute a funcion to get the position of a value).
Regards.
Pd:Excuse-me for mistakes with english.
You can also use a CLASS HELPER to re-program the "IndexOfName" function:
TYPE
TStringsHelper = CLASS HELPER FOR TStrings
FUNCTION IndexOfName(CONST Name : STRING) : INTEGER;
END;
FUNCTION TStringsHelper.IndexOfName(CONST Name : STRING) : INTEGER;
VAR
SL : TStringList ABSOLUTE Self;
S,T : STRING;
I : INTEGER;
BEGIN
IF (Self IS TStringList) AND SL.Sorted THEN BEGIN
S:=Name+NameValueSeparator;
IF SL.Find(S,I) THEN
Result:=I
ELSE IF (I<0) OR (I>=Count) THEN
Result:=-1
ELSE BEGIN
T:=SL[I];
IF CompareStrings(COPY(T,1,LENGTH(S)),S)=0 THEN Result:=I ELSE Result:=-1
END;
EXIT
END;
Result:=INHERITED IndexOfName(Name)
END;
(or implement it in a descendant TStrings class if you dislike CLASS HELPERs or don't have them in your Delphi version).
This will use a binary search on a sorted TStringList and a sequential search on other TStrings classes.

Getting a Field List from a DBExpress TSQLQuery

I am having a problem getting a list of fields from a query defined at run time by the users of my program. I let my users enter a SQL query into a memo control and then I want to let them go through the fields that will return and do such things as format the output, sum column values and so forth. So, I have to get the column names so they have a place to enter the additional information.
I would do fine if there were no parameters, but I also have to let them define filter parameters for the query. So, if I want to set the parameters to null, I have to know what the parameter's datatype is.
I am using Delphi 2006. I connect to a Firebird 2.1 database using the DBExpress component TSQLConnection and TSQLQuery. Previously, I was successful using:
for i := 0 to Qry.Params.Count - 1 do Qry.Params[i].value := varNull;
I discovered I had a problem when I tried to use a date parameter. It was just a coincidence that all my parameters up until then had been integers (record IDs). It turns out that varNull is just an enumerated constant with a value of 1 so I was getting acceptable results (no records) was working okay.
I only need a list of the fields. Maybe I should just parse the SELECT clause of the SQL statement. I thought setting Qry.Prepared to True would get me a list of the fields but no such luck. It wants values for the parameters.
If you have an idea, I would sure like to hear it. Thanks for any help.
Replied again 'coz I'm interested. My methods works (with my queries) because they have been pre-defined with the params' datatypes preset to the correct type:)
I'm not sure how you are expecting the query to know or derive the datatype of the param given that you are not even selecting the field that it operates against.
So I think your query setup and user input method will need more attention. I've just looked up how I did this a while ago. I do not use a parameterised query - I just get the "parameter values" from the user and put them directly into the SQL. So your sql would then read:
SELECT s.hEmployee, e.sLastName
FROM PR_Paystub s
INNER JOIN PR_Employee e ON e.hKey = s.hEmployee
WHERE s.dtPaydate > '01/01/2008'
therefore no parameter type knowledge is necessary. Does not stop your users entering garbage but that goes back to input control :)
Although a slightly different dataset type this is what I use with TClientDataset simple and effective :)
for i := 0 to FilterDataSet.Params.Count -1 do
begin
Case FilterDataSet.Params.Items[i].Datatype of
ftString:
ftSmallint, ftInteger, ftWord:
ftFloat, ftCurrency, ftBCD:
ftDate:
ftTime:
ftDateTime:
.
.
.
end;
end;
can you not do something similar with the query?
You guys are making this way too hard:
for i := 0 to Qry.Params.Count - 1 do begin
Qry.Params[i].Clear;
Qry.Params[i].Bound := True;
end;
I'm not sure what version of Delphi you are using. In the Delphi 2006 help under Variant Types, it says:
Special conversion rules apply to the
Borland.Delphi.System.TDateTime type
declared in the System unit. When a
Borland.Delphi.System.TDateTime is
converted to any other type, it
treated as a normal Double. When an
integer, real, or Boolean is converted
to a Borland.Delphi.System.TDateTime,
it is first converted to a Double,
then read as a date-time value. When a
string is converted to a
Borland.Delphi.System.TDateTime, it is
interpreted as a date-time value using
the regional settings. When an
Unassigned value is converted to
Borland.Delphi.System.TDateTime, it is
treated like the real or integer value
0. Converting a Null value to Borland.Delphi.System.TDateTime raises
an exception.
The last sentence seems important to me. I would read that as varNull cannot be converted to a TDateTime to put into the field, and hence you get the exception that you're experiencing.
It also implies that this is the only special case.
Couldn't you do something like:
for i := 0 to Qry.Params.Count - 1 do
begin
if VarType(Qry.Params[i].value) and varTypeMask = varDate then
begin
Qry.Params[i].value := Now; //or whatever you choose as your default
end
else
begin
Qry.Params[i].value := varNull;
end;
end;
What I ended up doing was this:
sNull := 'NULL';
Qry.SQL.Add(sSQL);
for i := 0 to Qry.Params.Count - 1 do begin
sParamName := Qry.Params[i].Name;
sSQL := SearchAndReplace (sSQL, ':' + sParamName, sNull, DELIMITERS);
end;
I had to write SearchAndReplace but that was easy. Delimiters are just the characters that signal the end of a word.
TmpQuery.ParamByName('MyDateTimeParam').DataType := ftDate;
TmpQuery.ParamByName('MyDateTimeParam').Clear;
TmpQuery.ParamByName('MyDateTimeParam').Bound := True;

Resources