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

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;

Related

Making TJvAppDBStorage work with FireDAC/Firebird

Using either Delphi 10.2 or 10.3, FireDAC and either Firebird 2.5 or 3.0: I've used the JVCL TJvAppStorage components for years and never had a problem with them, either to INI/XML storage or to a table in an AbsoluteDB database. I'm trying to migrate an app from AbsoluteDB to Firebird via FireDAC, and can't get the TJvAppDBStorage to write entries - it returns no errors, but nothing actually gets written into the table.
I have a datamodule containing the FireDAC connection and driver components, the JvAppDBStorage, a TDataSource and a TFDTable component. The FDB exists containing an appropriate table, the TFDTable is open on that table, the JvAppDBStorage has its properties set to match the table's fields, and the TFDTable, datasource and JvAppDBStorage are properly linked. (This all mirrors what has existed and worked against AbsoluteDB.) A call to dmStorage.FBStorage.WriteString(dmStorage.FBStorage.ConcatPaths(['General', 'LastStarted']), FormatDateTime(StdDTFmtStr, Now)); does not throw any exceptions, but nothing actually gets written into the table. Doing a normal append/set fields/post construct via the TFDTable works properly.
Any help appreciated!
Steve
There are 3 critical parts to this:
The underlying table must have a PK. I have an ID column with an associated sequence with before-insert trigger to make an auto-inc column for my PK, and an index that's a merge of the SectionID and KeyID fields to speed up the Locate() calls.
When creating the TFDTable object, you must create a new TFDTransaction object and attach it to the TFDTable and the TFDConnection.
On the TFDTable, you must set UpdateOptions.UpdateMode to upWhereKeyOnly. If you only do the first two steps, reads and appends will work but edits will not.
function TFDDBMgr.MakeTableObj(const ATblName: String): TFDTable;
begin
Result := TFDTable.Create(Self);
with Result do begin
//hook up all our needed stuff
Connection := Self.Connection;
TableName := ATblName;
CachedUpdates := False;
{...}
//!!.SS 09/11/19: REQUIRED for TJvAppDBStorage use,
//also requires that a PK be defined on the table
UpdateOptions.UpdateMode := upWhereKeyOnly;
Transaction := TFDTransaction.Create(Result);
Transaction.Connection := Self.Connection;
end;
end;

Max length TSQLConnection.Params values

Hello fellow StackOverflowers,
Currently I'm facing a situation where it seems that there is a maximum length for the Database property of a TSQLConnection object in Delphi.
When I open the connection to my database I get the following error when I use a rather long (154 chars) database name:
dbExpress Error: [0x0015]: Connection failed
SQL Server Error: unrecognized database parameter block
wrong version of database parameter block
When I relocate my database file to another location (and with that reduce the length of the path) it will connect to the database.
I am currently using the Object Inspector to set the connection properties of the TSQLConnection object.
Basically, my question comes down to this:
Does a TSQLConnection have a maximum length for the values set in the Params property? And if so, what is the maximum length of these values?
Update
I've found two ways to open a copy of Employee.Gdb in a folder with a 160-character name ('abcdefghij0123456789' x 8).
What I did firstly was to edit the DBXConnections.Ini file and changed the Database parameter in the [IBConnection] section to read
Database=localhost:D:\abcdefghij01234567890abcdefghij01234567890abcdefghij01234567890abcdefghij01234567890abcdefghij01234567890abcdefghij01234567890abcdefghij01234567890abcdefghij01234567890\employee.gdb
Then, I can successfully connect to it, open the Employee.Gdb and make changes to the Customer table. I have verified the changes in IBConsole just in case the copy of Employee.Gdb wasn't the one I assumed it was.
Subsequently, I've found that I can create and open the db in code using Delphi Seattle and Interbase XE7, as follows:
function LongPath : String;
begin
Result := 'D:\' + DupeString('abcdefghij0123456789', 8);
end;
function LongDBName : String;
begin
Result := LongPath + '\Employee.Gdb';
end;
procedure TForm1.OpenDB;
var
Ini : TMemIniFile;
const
scDBXConIni = 'C:\Users\Public\Documents\Embarcadero\Studio\dbExpress\17.0\dbxconnections.ini';
scSourceDB = 'D:\Delphi\Databases\Interbase\Employee.Gdb';
begin
Ini := TMemIniFile.Create(scDBXConIni);
try
// First, blank out the Database value in the IBConnection section
// of DBXConnections.Ini
Ini.WriteString('IBConnection', 'Database', '');
Ini.UpdateFile;
// Next, create the long-named directory and copy Employee.Gdb to it
if not DirectoryExists(LongPath) then
MkDir(LongPath);
Assert(CopyFile(PChar(scSourceDB), PChar(LongDBName), False));
// Set LoadParamsOnConnect to False so that the SqlConnection uses
// the value of the Database we are about to give it
SqlConnection1.LoadParamsOnConnect := False;
SqlConnection1.Params.Values['Database'] := LongDBName;
SqlConnection1.Connected := True;
// Open the CDS to view the data
CDS1.Open;
finally
Ini.Free;
end;
end;
The critical step in doing it this way is setting LoadParamsOnConnect to False, which I confess I'd overlooked in earlier attempts to get this code to work.
I've got some earlier versions of Delphi on this machine, so if you're not using Seattle and the above code doesn't work for you, tell me which one you are using and I'll see if I can try that.
**[Original answer]
Actually, I think that this may be an error occurring in one of the DBX DLLs.
I created a folder with a 160-character name, then copied the demo Employee.Gdb database into it. Interbase XE7's IBConsole can open the db without error. So could a small test project contructed with IBX components in Delphi Seattle.
However, with an equivalent DBX project, when I use the code below
procedure TForm1.Button1Click(Sender: TObject);
begin
SqlConnection1.Params.Values['database'] := 'D:\abcdefghij01234567890abcdefghij01234567890abcdefghij01234567890abcdefghij01234567890abcdefghij01234567890abcdefghij01234567890abcdefghij01234567890abcdefghij01234567890\employee.gdb';
SqlConnection1.Connected := True;
end;
I get an error in
procedure TDBXDynalinkConnection.DerivedOpen;
var
Count: TInt32;
Names: TWideStringArray;
Values: TWideStringArray;
IsolationLevel: Longint;
DBXError: TDBXErrorCode;
begin
Count := FConnectionProperties.Properties.Count;
FConnectionProperties.GetLists(Names, Values);
CheckResult(FMethodTable.FDBXConnection_Connect(FConnectionHandle, Count, Names, Values));
DBXError := FMethodTable.FDBXConnection_GetIsolation(FConnectionHandle, IsolationLevel);
'I/O error for file "database.gdb"
Error while trying to open file
The operation completed successfully'
and the Database param of the SqlConnection is left at the value 'Database.Gdb', which is not the value I specified, of course, nor was it the value specified in the params in the IDE, which was 'd:\delphi\databases\interbase\employee.gdb'.
I wondered if I could work around this problem by SUBSTing a drive to the 'abcdefg ...' path. I tried that and opening the database as "x:\employee.gdb" , but I get the same error in my DBX app, and also IBConsole cannot access the db either.
I think you need a shorter physical path!**
This is related to MSSql Server:
As a general guideline, long path names greater than 160 characters
might cause problems.
from Microsoft TechNet - https://technet.microsoft.com/en-us/library/ms165768(v=sql.105).aspx

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.

Everytime I try to communicate with my database with stored procedures I get this "Cannot perform this operation on a closed dataset"

I'm working on a Delphi project with a MS SQL Server database, I connected the database with ADOConnection, DataSource and ADOProc components from Borland Delphi 7 and I added this code in behind:
procedure TForm1.Button2Click(Sender: TObject);
begin
ADOStoredProc1.ProcedureName := 'sp_Delete_Clen';
ADOStoredProc1.Refresh;
ADOStoredProc1.Parameters.ParamByName('#clenID').Value := Edit6.Text;
ADOStoredProc1.Active := True;
ADOStoredProc1.ExecProc;
end;
The component Edit6 is an editbox that takes the ID of the tuple that should be deleted from the database and ADOStoredProc1 is the stored procedure in the database that takes 1 parametar (the ID you want to delete).
The project runs with no problems, I even got a TADOTable and a DBGrid that load the information from the database, but when I try to delete a tuple from the database using its ID written in the EditBox I get this Error: "Cannot perform this operation on a closed dataset" and the breakpoint of the project is when the application tries to add the value for the 'clenID' parameter.
Where is my mistake and how to fix it?
I think the ADOStoredProc1.Refresh method is not appropriate here. In this case the stored procedure does not return a result set. Could you leave it out? And also the line ADOStoredProc1.Active := True. The connection to the database is open I presume? Could you also check the values of the Parameters collection in the Object Inspector?
I think you want to call ADOStoredProc1.Parameters.Refresh, not ADOStoredProc1.Refresh.
Also, you should only set Active to True if the SQL Server Stored procedure returns a dataset - i.e. the result of a SELECT statement. Setting Active to True is the same as calling Open.
If the stored procedure only returns a result code (RETURN n), then use ExecProc.
In no case should you use both ADOStoredProc1.Active := True; and ADOStoredProc1.ExecProc;
In summary, you probably want something like
procedure TForm1.btnDeleteClick(Sender: TObject);
begin
ADOStoredProc1.ProcedureName := 'sp_Delete_Clen';
ADOStoredProc1.Parameters.Refresh; // gets the parameter list from SQL Server
ADOStoredProc1.Parameters.ParamByName('#clenID').Value := edtID.Text;
ADOStoredProc1.ExecProc;
end;

ADO, Adonis, Update Criteria

On a form, I have a Quantum Grid and some db-aware editcomponents. When appending a new record in the grid, typing some editvalues both in the grid and the separate editcompoennts, I get an error:
EOleException: Row cannot be located for updating. Some values may have been chenged since it was last read
After some googling, I think changing the 'Update Criteria'-property from adCriteriaAllCols to adCriteriaKey may be the right solution. But how, and when, do I do that on a Adonis query?
if your dataset contains an autoincrement field or one or more fields have default values then this may be the problem. After calling Post fields change their value in the db but that may not be detected by your dataset
Although I don't use Adonis, I believe there is a possibility it use the same mechanism TClientDataset (CDS) uses to identify the fields that compose the primary key: the property TField.ProviderFlags.
I believe it could be a good place to start looking.
Actually ADO provides dynamic property to control Query Based Update (QBU) behavior
Most of code is covered in ADOInt.pas, the appropriate event is OnAfterOpen for recordset properties (any TADODataSet), and OnCreate for connection properties (TADOConnection).
I guess ‘Update Criteria’ is not the solution in this case, since it deals with WHERE clause to specify fields to be used for updates.
You may change 'Update Resync' as follows:
//After open a TCustomADODataSet
TCustomADODataSet(DataSet).Properties['Update Resync'].Value :=
adResyncAutoIncrement + adResyncUpdates + adResyncInserts;
if you have join table in your query :
TADOQuery cannot be edit and post data in sql database.
you can create other TADOQuery and select by primary key without any join table and edit and then post data in sql database.
I had a similar problem - Updating a row where all values were the same would result in the error you're getting. I added the flag "Option=2" to the end of my ado connection and this fixed the issue.
This is old. but my situation may happens for someone else. so I post this answer.
This Error happened for me too.
I was using Access database and using some TADOTable on the form.
the relation was master-detail and I connected all of the tables with the IDE Designer together.
my tables were tbl_Floor,tbl_FloorParts,tbl_Seat which tbl_Floor was master of tbl_FloorParts and tbl_FloorParts was master of tbl_Seat.
So for solving this error I did this trick.
procedure Tfrm_Main.UpdateTblFloor(...);
var
FID:Integer;
q:TADOQuery
begin
FID:=tbl_Floor.FieldByName('FID').AsInteger;
tbl_Floor.Close;
q:=TADOQuery.Create(nil);
try
q.Connection:=tbl_Floor.Connection;
q.SQL.Add('Update [Floor]');
q.SQL.Add(...);//Set Fields that needed to be updated
q.SQL.Add('where [FID]='+IntToStr(FID));
q.ExecSQL;
finally
q.free;
end;
tbl_Floor.Open;
tbl_Floor.Locate('FID',FId,[loPartialKey]);
end;
and I added these events for tbl_Floor,tbl_FloorParts
procedure Tfrm_Main.tbl_FloorAfterOpen(DataSet: TDataSet);
begin
tbl_FloorParts.Open;
end;
procedure Tfrm_Main.tbl_FloorBeforeClose(DataSet: TDataSet);
begin
tbl_FloorParts.Close;
end;
procedure Tfrm_Main.tbl_FloorPartsAfterOpen(DataSet: TDataSet);
begin
tbl_Seat.Open;
end;
procedure Tfrm_Main.tbl_FloorPartsBeforeClose(DataSet: TDataSet);
begin
tbl_Seat.Close;
end;

Resources