Delphi DBGrid date format for Firebird timestamp field - delphi

I display the content of a Firebird database into a TDBgrid. The database has a 'TIMESTAMP' data type field that I would like to display with date/time format:
'YYYY/MM/DD HH:mm:ss'. (Now its displayed as 'YYMMDD HHmmss')
How to achieve this?
I tried this:
procedure TDataModule1.IBQuery1AfterOpen(DataSet: TDataSet);
begin
TDateTimeField(IBQuery1.FieldByName('timestamp_')).DisplayFormat := 'YYYY/MM/DD HH:mm:ss';
end;
But this causes some side effects at other parts of the program, so its not an alternative. For example at the 'IBQuery1.Open' statement I get the '...timestamp_ not found...' debugger message in the method that I clear the database with.
function TfrmLogger.db_events_clearall: integer;
begin
result := -1;
try
with datamodule1.IBQuery1 do begin
Close;
With SQL do begin
Clear;
Add('DELETE FROM MEVENTS')
end;
if not Prepared then
Prepare;
Open; //Exception here
Close;
Result := 1;
end;
except
on E: Exception do begin
ShowMessage(E.ClassName);
ShowMessage(E.Message);
Datamodule1.IBQuery1.close;
end;
end;
end;
I get the same exception message when trying to open the query for writing into the database.
*EDIT >>
I have modified the database clear as the following:
function TfrmLogger.db_events_clearall: integer;
var
IBQuery: TIBQuery;
IBTransaction: TIBTransaction;
DataSource: TDataSource;
begin
result := -1;
//Implicit local db objects creation
IBQuery := TIBQuery.Create(nil);
IBQuery.Database := datamodule1.IBdbCLEVENTS;
DataSource := TDataSource.Create(nil);
DataSource.DataSet := IBQuery;
IBTransaction := TIBTransaction.Create(nil);
IBTransaction.DefaultDatabase := datamodule1.IBdbCLEVENTS;
IBQuery.Transaction := IBTransaction;
try
with IBQuery do begin
SQL.Text := DELETE FROM MSTEVENTS;
ExecSQL;
IBTransaction.Commit;
result := 1;
end;
except
on E : Exception do
begin
ShowMessage(E.ClassName + ^M^J + E.Message);
IBTransaction.Rollback;
end;
end;
freeandnil(IBQuery);
freeandnil(DataSource);
freeandnil(IBTransaction);
end;
After clearing the database yet i can load the records into the dbgrid, seems like the database has not been updated. After the program restart i can see all the records been deleted.

The whole function TfrmLogger.db_events_clearall seems very dubious.
You do not provide SQL_DELETE_ROW but by the answer this does not seem to be SELECT-request returning the "resultset". So most probably it should NOT be run by ".Open" but instead by ".Execute" or ".ExecSQL" or something like that.
UPD. it was added SQL_DELETE_ROW = 'DELETE FROM MEVENTS'; confirming my prior and further expectations. Almost. The constant name suggests you want to delete ONE ROW, and the query text says you delete ALL ROWS, which is correct I wonder?..
Additionally, since there is no "resultset" - there is nothing to .Close after .Exec.... - but you may check the .RowsAffected if there is such a property in DBX, to see how many rows were actually scheduled to be deleted.
Additionally, no, this function DOES NOT delete rows, it only schedules them to be deleted. When dealing with SQL you do have to invest time and effort into learning about TRANSACTIONS, otherwise you would soon get drown in side-effects.
In particular, here you have to COMMIT the deleting transaction. For that you either have to explicitly create, start and bind to the IBQuery a transaction, or to find out which transaction was implicitly used by IBQuery1 and .Commit; it. And .Rollback it on exceptions.
Yes, boring, and all that. And you may hope for IBX to be smart-enough to do commits for you once in a while. But without isolating data changes by transactions you would be bound to hardly reproducible "side effects" coming from all kinds of "race conditions".
Example
FieldDefs.Clear; // frankly, I do not quite recall if IBX has those, but probably it does.
Fields.Clear; // forget the customizations to the fields, and the fields as well
Open; // Make no Exception here
Close;
Halt; // << insert this line
Result := 1;
Try this, and I bet your table would not get cleared despite the query was "opened" and "closed" without error.
The whole With SQL do begin monster can be replaced with the one-liner SQL.Text := SQL_DELETE_ROW;. Learn what TStrings class is in Delphi - it is used in very many places of Delphi libraries so it would save you much time to know this class services and features.
There is no point to Prepare a one-time query, that you execute and forget. Preparation is done to the queries where you DO NOT CHANGE the SQL.Text but only change PARAMETERS and then re-open the query with THE SAME TEXT but different values.
Okay, sometimes I do use(misuse?) explicit preparation to make sure the library fetches parameters datatypes from the server. But in your example there is neither. Your code however does not use parameters and you do not use many opens with the same neverchanging SQL.text. Thus, it becomes a noise, making longer to type and harder to read.
Try ShowMessage(E.ClassName + ^M^J + E.Message) or just Application.ShowException(E) - no point to make TWO stopping modal windows instead of one.
Datamodule1.IBQuery1.close; - this is actually a place for rolling back the transaction, rather than merely closing queries, which were not open anyway.
Now, the very idea to make TWO (or more?) SQL requests going throw ONE Delphi query object is questionable per se. You make customization to the query, such as fixing DisplayFormat or setting fields' event handlers, then that query is quite worth to be left persistently customized. You may even set DisplayFormat in design-time, why not.
There is little point in jockeying on one single TIBQuery object - have as many as you need. As of now you have to pervasively and accurately reason WHICH text is inside the IBQuery1 in every function of you program.
That again creates the potential for future side effects. Imagine you have some place where you do function1; function2; and later you would decide you need to swap them and do function2; function1;. Can you do it? But what if function2 changes the IBQuery1.SQL.Text and function1 is depending on the prior text? What then?
So, basically, sort your queries. There should be those queries that do live across the function calls, and then they better to have a dedicated query object and not get overused with different queries. And there should be "one time" queries that only are used inside one function and never outside it, like the SQL_DELETE_ROW - those queries you may overuse, if done with care. But still better remake those functions to make their queries as local variables, invisible to no one but themselves.
PS. Seems you've got stuck with IBX library, then I suggest you to take a look at this extension http://www.loginovprojects.ru/download.php?getfilename=uploads/other/ibxfbutils.zip
Among other things it provides for generic insert/delete functions, which would create and delete temporary query objects inside, so you would not have to think about it.
Transactions management is still on you to keep in mind and control.

Related

Why fires the AfterScroll event of a nested (detail) dataset more than once when its master dataset scrolled to another record? How to deal with it?

I have a Delphi program with 3 successive nested (master-details) FireDAC datasets namely: SuperMaster, Master and Details dataset.
In the AfterScroll event of the Details dataset only, new records are inserted into the database.
My program logic needs the AfterScroll event of the Details dataset to fire only once: when any of the parent datasets (SuperMaster or Master) scrolled to another record. The AfterScroll event should not trigger more than once because this will cause my logic to insert wrong records.
The problem is that when scrolling occurs to any of the parent datasets (from the current record to another one, even to the adjacent one), i.e. moving a single record only, the Details dataset AfterScroll event fired more than once and sometimes more than twice!
I do not want to mess with the original FireDac classes by overriding its AfterScroll event to force it to do my logic only once, nor to add a Boolean flag to my logic so that it runs only once.
I tried to put all the tables into one view, but it costs me to change the program logic and make future updates more demanding.
I searched and read many articles and a book ("Delphi in Depth: FIREDAC" by Cary Jensen), but could not find a solution nor know why or how it occurs!
Why does this happen? I want to understand the basics please. Is there any elegant solution?
Short of changing the FireDAC source (which I think would be a thoroughly bad idea - beware of opening Pandora's box) I don't think your goal of avoiding the
use of a Boolean flag is achievable. It should however be trivial to achieve if you do use a Boolean flag.
I also wonder whether you observation that the Detail's AfterScroll event occurs more than once per detail dataset
is correct, if that is what you are saying. If it is, you should edit your q to include a minimal, reproducible example, preferably based on the code below.
TDataSet and its descendants, including all the FireDAC datasets operate to a very tightly designed
state machine which includes the handling of master-detail behaviour that's been tested by all the TDataSet
usage since Delphi was first released. Trying to mess with that would invite
disaster.
If you try the minimal VCL project below, I think you'll find it's not very hard to satisfy yourself that
the Detail's AfterScroll event behaves exactly as you would expect from the coding of TDataSet's and FireDAC's source.
Running the code you will find that the breakpoint in DetailAfterScroll trips 4 times before
the first breakpoint on MasterFirst. The next time the BP trips, observe the call stack via
View | Debug Windows | Call stack; if you look down the cal stack, you'll find that the call to Detail.AfterScroll
was ultimately called via the penultimate line of
procedure TFDMasterDataLink.DataEvent(Event: TDataEvent; Info: NativeInt);
An FDMasterDataLink is automatically created to handle the triggering of Detail data events based on
the operation of the Master. And that, really, is the end of the story. because however much you might
disagree with this behaviour , you can't really do anything about it short of using a Boolean flag
in your own code.
I think it would be wise to verify that DetailAfterScroll is only being called once per dataset that's
on the detail side of the Master. If it's happening more than once, it would be worth checking that
it isn't your own code (or linking together of DataSets) that's causing it.
As you'll see, nearly all of what the example does is defined in the OnCreate event
to avoid having to use the Object Inspector to set up the components, so that is should "just work".
Code:
type
TForm1 = class(TForm)
Master: TFDMemTable;
Detail: TFDMemTable;
DataSource1: TDataSource;
procedure FormCreate(Sender: TObject);
procedure DetailAfterScroll(DataSet: TDataSet);
public
DetailsScrolled : Integer;
end;
[...]
procedure TForm1.DetailAfterScroll(DataSet: TDataSet);
begin
// Place debugger breakpoint on the following line
Inc(DetailsScrolled);
end;
procedure TForm1.FormCreate(Sender: TObject);
var
AField : TField;
begin
AField := TIntegerField.Create(Self);
AField.FieldName := 'MasterID';
AField.DataSet := Master;
Master.CreateDataSet;
AField := TIntegerField.Create(Self);
AField.FieldName := 'DetailID';
AField.DataSet := Detail;
AField := TIntegerField.Create(Self);
AField.FieldName := 'MasterID';
AField.DataSet := Detail;
Detail.CreateDataSet;
DataSource1.DataSet := Master;
Detail.MasterSource := DataSource1;
Detail.MasterFields := 'MasterID';
Detail.IndexFieldNames := 'MasterID;DetailID';
Master.InsertRecord([1]);
Master.InsertRecord([2]);
Detail.InsertRecord([1, 1]);
Detail.InsertRecord([2, 1]);
Detail.InsertRecord([3, 2]);
// Place debugger breakpoint on EACH of the following three lines
Master.First;
Master.Next;
Master.First;
end;

Does TClientDataSet.ApplyUpdates(0) require to execute CheckBrowseMode/Post before?

I have TIBQuery-TDataSetProvider-TClientDataSet chain in Delphi 2009 (Firebird 3.0) and I execute MyClientDataSet.ApplyUpdates(0). Am I required to call CheckBrowseMode or Post on this CDS before calling ApplyUpdates(0). I am almost sure that I am required to call Post/CheckBrowseMode and I think that no-posted updates will not be applied to the IBQuery, I have no documentation for/against such thinking but it is logical to think so. But sometimes I can observe that MyClientDataSet is in [dsInsert, dsEdit] state before ApplyUpdates(0) and the new values are still posted and saved in query. But there are evidence and reason against that as well. So - I am confused.
Of course, I don't use CachedUpdated for TIBQuery (because there is CDS) and this is not the question about commiting of transactions, I strongly control them and that issue is excluded.
I made test: I put raise Exception in the BeforePost event of MyClientDataSet and MadException gives trace:
TMyDM.MyClientDataSetBeforePost
TDataSet.DoBeforePost
TDataSet.Post
TCustomClientDataSet.Post
TDataSet.CheckBrowseMode
TCustomClientDataSet.ApplyUpdates
So, there is empirical evidence, that CheckBroseMode is called automatically, but is it by accident (e.g. due to some special configuration of DataSetProvider or ClientDataSet) or is it rule that I can find in the documentation?
or is it rule that I can find in the documentation?
There is a simpler rule, which should be self-evident, I would have thought: If the CDS's state is dsEdit or dsInsert, one might think of it as being in a change-pending state - changes have made (e.g. in a DB-aware control) but not yet been written back to the CDS's record buffer (or buffers if it has nested data). Otoh, the point of calling ApplyUpdates is to write changes in the record buffer back to the source dataset via the DSP. It would seem elementary therefore that one should not attempt to call ApplyUpdates while the CDS is in dsEdit/dsInsert. In any case, CheckBrowse mode is called by TCustomClientDataSet.ApplyUpdates:
function TCustomClientDataSet.ApplyUpdates(MaxErrors: Integer): Integer;
var
RootDataset: TCustomClientDataset;
begin
CheckBrowseMode;
RootDataset := Self;
while RootDataset.FParentDataSet <> nil do
RootDataset := RootDataset.FParentDataset;
with RootDataset do
if ChangeCount = 0 then
Result := 0 else
Reconcile(DoApplyUpdates(Delta, MaxErrors, Result));
end;
It is worth noting that if a dataset's state is dsEdit or dsInsert, CheckBrowseMode does one of three things:
Nothing, except calling CheckActive and then DataEvent(deCheckBrowseMode, 0),
if the dataset's state is not dsEdit, dsInsert or dsSetKey
Calls Post if the current record had been modified
Calls Cancel otherwise.
procedure TDataSet.CheckBrowseMode;
begin
CheckActive;
DataEvent(deCheckBrowseMode, 0);
case State of
dsEdit, dsInsert:
begin
UpdateRecord;
if Modified then Post else Cancel;
end;
dsSetKey:
Post;
end;
end;

What is the best way in Delphi to generate a list of forms/units in the order they load?

I had an issue where a file kept deleting on startup and I couldn't track down the code responsible. I wound up adding Vcl.Dialogs to all the units and creating an initialization section that looked like this:
initialization
begin
ShowMessage('Inside [Unit Name Here]');
end;
This was quite a pain. Is there an easy way to generate a list of forms/units in the order in which they fire off?
UPDATE: 2019-08-01 (Helpful MAP links)
Here are two links that may assist in understanding DELPHI map files
http://docwiki.embarcadero.com/RADStudio/Rio/en/API_%28%2A.map%29
Understanding Delphi MAP File
You really didn't need to go to all that trouble modifying your source units. I think you'll find that using the method below will find the misbehaving unit
much more quickly than somehow generating a list of units and then ploughing
your way through it.
If you look in System.Pas, you'll find a procedure InitUnits like this (from D7).
procedure InitUnits;
var
Count, I: Integer;
Table: PUnitEntryTable;
P: Pointer;
begin
if InitContext.InitTable = nil then
exit;
Count := InitContext.InitTable^.UnitCount;
I := 0;
Table := InitContext.InitTable^.UnitInfo;
[...]
try
while I < Count do
begin
P := Table^[I].Init;
Inc(I);
InitContext.InitCount := I;
if Assigned(P) then
begin
TProc(P)();
end;
end;
except
FinalizeUnits;
raise;
end;
end;
This is the code which causes the initialization code of each unit to be called. It works its way through the units and calls the initialization section (if any)
of each unit via the call
TProc(P)();
You can inspect the value of Count prior to the loop; don't be surprised if its upwards
of a couple of hundreds even for a relatively simple project.
Put a breakpoint on the TProc(P)(); line and right-click and set the PassCount to
half the value of Count. Run your app and when the breakpoint trips, check whether
the file has been deleted.
You can then do a binary search through the values of
Count (by continuing the current run if the file is still there, or resetting the app
and halving the Pass Count) to establish exactly which unit causes the file to be deleted.
Because you can use a binary search to do this, it will rapidly converge on the
unit which is deleting the file. Of course, you can trace into the unit's
initialization code (if it has been compiled with debug info) when the breakpoint
trips by pressing F7 on TProc(P)();
You can inspect the segments section of the map file. The entries with C=ICODE are those units with initialization parts in the order they are executed.

Using FireDac's onUpdateRecord and optionally execute default statements

I have a query that returns rows with an outer join. This causes records to exist in the results that don't really exist in the table. When those rows are changed FireDac sees the change as an Update instead of an insert. That behavior makes sense from FireDac's side because it has no way to tell the difference.
I am overriding the OnUpdateRecord event to catch those rows that are marked wrong and perform the insert myself. That part is working great. What I can't figure out is how to tell FireDac to perform it's normal process on other records. I thought I could set the AAction to eaDefault and on the return FireDac would continue to process the row as normal. However, that does not seem to be the case. Once OnUpdateRecord is in place it looks like FireDac never does any updates to the server.
Is there a way to tell FireDac to update the current row? Either in the OnUpdateRecord function by calling something - or maybe a different return value that I missed?
Otherwise is there a different way to change these updates into inserts? I looked at changing the UpdateStatus but that is read only. I looked at TFDUpdateSql - but I could not figure out a way how to only sometimes turn the update into an insert.
I am using CachedUpdates if that makes any difference.
Here is what I have for an OnUpdateRecord function:
procedure TMaintainUserAccountsData.QueryDrowssapRolesUpdateRecord(
ASender: TDataSet; ARequest: TFDUpdateRequest; var AAction: TFDErrorAction;
AOptions: TFDUpdateRowOptions);
{^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^}
begin
if (ARequest = arUpdate) and VarIsNull(ASender.FieldByName('Username').OldValue) then
begin
ASender.Edit;
ASender.FieldByName('RoleTypeID').Value := ASender.FieldByName('RealRoleTypeID').Value;
ASender.Post;
MGRDataAccess.ExecSQL('INSERT INTO DrowssapRoles (Username, RoleTypeID, HasRole) VALUES (:Username, :RoleTypeID, :HasRole)',
[ASender.FieldByName('Username').AsString, ASender.FieldByName('RoleTypeID').AsInteger,
ASender.FieldByName('HasRole').AsBoolean]);
AAction := eaApplied;
end
else
begin
// What do I do here to get the default FireDac actions?
end;
end;

Disabling the login prompt without using the TDatabase bypass

I am currently trying to connect to a database using an ODBC Alias to SQL Server. The problem I'm having is that when I use my TQuery object to get the information it always requests login details (nevermind whether I've specified them in the ODBC creation). I don't mind manually setting them in the code, but I can't find how to do that.
The most common solution I've found is to use the database component and go through that. However that comes with its own issues. Due to my dataset being so large and the database component converting the dataset to a Paradox table I keep getting a BDE error of 'Temporary Table Resource Limit'.
I don't get this error if I ignore the database component (which is fine) however this leaves me with the login prompt issue. Has anyone found a way to bypass this for TQuerys without swapping to other connection paths such as ADO?
I'm a bit rusty with the BDE but I don't think there's an easy way to avoid the login prompt if what you're saying is that you're not using a TDatabase component in your project.
The reason is that when you attempt to open your TQuery without a TDatabase (or TSession) component in your project, the default Session object in your app will call the routine below from within your TQuery's OpenCursor:
{ from DBTables.Pas }
function TSession.DoOpenDatabase(const DatabaseName: string; AOwner: TComponent): TDatabase;
var
TempDatabase: TDatabase;
begin
Result := nil;
LockSession;
try
TempDatabase := nil;
try
Result := DoFindDatabase(DatabaseName, AOwner);
if Result = nil then
begin
TempDatabase := TDatabase.Create(Self);
TempDatabase.DatabaseName := DatabaseName;
TempDatabase.KeepConnection := FKeepConnections;
TempDatabase.Temporary := True;
Result := TempDatabase;
end;
Result.Open;
Inc(Result.FRefCount);
except
TempDatabase.Free;
raise;
end;
finally
UnLockSession;
end;
end;
As you can see, if the session can't find an existing TDatabase component with the right name, it creates a temporary one, and it's the call to Result.Open that pops up the login prompt, without, so far as I can see, giving you any opportunity to supply the password + user name before the pop-up (the Session's OnPassword doesn't seem to get called in the course of this).
Obviously you need to check using the debugger that that's what's happening in your app, a temporary TDatabase being created, I mean.
If what I've suggested in the Update below didn't work and I were desperate to avoid using a TDatabase component, I would look into the possibility of maybe deriving a TQuery descendant, and trying to override its OpenCursor to see if I could jam in the user name/password.
Anyway, seeing as you say you're not using an explicit TDatabase, if I understand you correctly, because of the "Temporary Table ..." issue, and seeing as the Session will create a temporary one anyway, I suppose it might be worth your while investigating why the temporary one doesn't provoke the "Temporary Table" error, whereas using a TDatabase component in your app evidently does. Idapi32.Cfg configuration issue, maybe? At the moment, I can't help you with that because I can't reproduce your "Temporary Table" error, despite using my TQuery to do a SELECT on a SqlServer table to return 250,000+ rows.
Oh, that's a point: Does your table contain any BLOBs? I seem to recall there's an Idapi config parameter that lets you reduce the temporary storage space the BDE uses for BLOBs (to zero, maybe, but it's been a long time since I used the BDE "for real").
Update: The thought just occurred to me that since your query seems to work with Session dynamically creating a TDatabase object, maybe it would also work with a TDatabase which you dynamically create yourself. I just tried the following, and it works for me:
procedure TForm1.DatabaseLogin(Database: TDatabase;
LoginParams: TStrings);
begin
LoginParams.Add('user name=sa');
LoginParams.Add('password=1234');
end;
procedure TForm1.Button1Click(Sender: TObject);
var
ADatabase : TDatabase;
begin
ADatabase := TDatabase.Create(Self);
ADatabase.AliasName := 'MAT41032';
ADatabase.DatabaseName := 'MAT41032';
ADatabase.SessionName := 'Default';
ADatabase.OnLogin := DatabaseLogin;
Query1.Open;
end;
+1 for an interesting question, btw.

Resources