Create table with firedac without SQL Script - database

Firedac library centralizes database behavior and have a lot of methods which works fine without care about the Database Type. Actually, using native drivers for most common databases, Firedac hides subtle differences on syntax allowing very flexible changes of database platform.
For example, generators and autoinc fields are easily detectable, CAST and parameters works fine allowing easy migration between databases.
How to use Firedac power to create New Table without instantiate FDQuery, which runs a SQL Script CREATE TABLE?
I hope to create any Object and, calling specific FieldByName for each Object Field, record it on database, but first I need to certify:
If Table is already created
If Field is already created
If record is already created
This is the code I have, so far:
TRecCustomer = record
Id:integer;
Name:String;
Birthday:TDate;
end;
ICustomer = interface
procedure setCustomerId(Value: Integer);
procedure setCustomerName(Value: String);
procedure SetBirthday(Value: TDate);
procedure Post;
end;
TCustomer = class(TInterfacedObjet, ICustomer)
CustomerObject=TRecCustomer;
procedure setCustomerId(Value: Integer);
procedure setCustomerName(Value: String);
procedure SetBirthday(Value: TDate);
procedure Post;
end;
procedure TCustomer.Post;
begin
if not TableExists('Customer') then CreateTable('Customer');
if not FieldExists('Name') then CreateField('Customer','name',ftString,[],40);
if not FieldExists('Id') then CreateField('Customer','Id',ftInteger,[cAutoInc,cNotNull]);
if not FieldExists('Birthday') then CreateField('Customer','birthday',ftDate);
end;
Imagine the procedures
CreateTable(Tablename: String)
CreateField(FieldName: String; FieldType: TDataType; Constraints: TConstraints; Length: Integer = 0);
where
TConstraints = set of (cAutoInc, cNotNull, cUnique, cEtc);
I can do it for specific database, for example Sqlite or Firebird, but I don't know hou to do for any database using Firedac resources.
I found FireDAC.Comp.Client.TFDTable.CreateTable(ARecreate: Boolean = True; AParts: TFDPhysCreateTableParts = [tpTable .. tpIndexes]), suggested by #Ondrej Kelle but I don't understood AParts usage. Somebody have an example? http://docwiki.embarcadero.com/Libraries/Berlin/en/FireDAC.Comp.Client.TFDTable.CreateTable
Thanks a lot.

You can create a TFDTable object, describe your table at least by specifying TableName and adding field definitions in the FieldDefs collection. In practice you'll usually create also a primary key index for some field (that's what can be done by the AddIndex method). After you describe your table, call CreateTable method. A minimal example can be:
var
Table: TFDTable;
begin
Table := TFDTable.Create(nil);
try
Table.Connection := FDConnection1;
{ specify table name }
Table.TableName := 'MyTable';
{ add some fields }
Table.FieldDefs.Add('ID', ftInteger, 0, False);
Table.FieldDefs.Add('Name', ftString, 50, False);
{ define primary key index }
Table.AddIndex('pkMyTableID', 'ID', '', [soPrimary]);
{ and create it; when the first parameter is True, an existing one is dropped }
Table.CreateTable(False);
finally
Table.Free;
end;
end;

Related

Storing Files in SQL Server Table (varbinary(max) column) using ADOQuery Component

I'm using SQL Server 2019 and Delphi 10.3.
I need to store any kind of files ( like pdf, txt, docx, etc) in a 'Personal_Files' table.
This table is composed by a column with the file extension ( as varchar) and a varbinary(max) column to store the file itself.
I did some research on how to store these files on a table, but without success. Below some example:
var
Input,Output: TStream;
FName: TFileName;
begin
...
//Create Streams and encode Base64:
Input := TFileStream.Create(FName,fmOpenRead);
Output := TFileStream.Create(FName+'Temp',fmCreate);
TNetEncoding.Base64.Encode(Input,Output);
... // Some validations
// In the ADOQuery component, I did this:
with ADOQuery, sql do
begin
close;
clear;
add('INSERT INTO MyDatabase.dbo.MyFilesTable (EXTENSION,FILEBIN)');
add('VALUES (:wextension, :wfilebin)');
Parameters.ParamByName('wextension').Value := TPath.GetExtension(FName);
Parameters.ParamByName('wfilebin').Value := Output.toString;
ExecSQL;
end;
In this example, I tried to parse the stream as String, after the encode, but when I look in the SQL Table, it's the same stream for all the archives I tried. The parameter doesn't accept TStream type. Thank you in advance.
After some research, and some advices, I found a way to send the file to my SQL Server table with ADOQuery. Altough, I learned that this isn't recommended, so it's just to answer my question directly:
Just a change on the final part of the code answers my question ( but again, it's not the recommended way to store files, as commented on the question.):
with ADOQuery, sql do
begin
close;
clear;
add('INSERT INTO MyDatabase.dbo.MyFilesTable (EXTENSION,FILEBIN)');
add('VALUES (:wextension, :wfilebin)');
Parameters.ParamByName('wextension').Value := TPath.GetExtension(FName);
Parameters.Items[1].LoadFromStream(Output,ftVarBytes);
ExecSQL;
end;
Just changing the way I was setting the parameter solved the problem. In this example, using the 'LoadFromStream' on the Paremeters.Items[n], where n is the parameter index, it worked very well. The ftVarBytes is the field type parameter.

Reading DateTime from Database using Delphi and FireDAC

I'm failing dismally to read a datetime from an SQLite database using Delphi 10.3 and FireDAC.
As the simplest example, I create a sample database using sqlite as follows:
.open Test.db
CREATE TABLE "TABLE1" ("Name"VarChar(16), "Time" datetime);
INSERT INTO Table1 (Name,Time) VALUES("Fred",time('now'));
Then
select * from Table1
gives Fred|16:52:57 as expected.
If I generate a Delphi program with an FDConnection1 and FDQuery1 linked to a datasource and DBgrid it will read "Fred" but not the time. The value returned by FDquery1 asstring is '' and asfloat is 0.
If I try the FireDAC explorer tool to look at the database it also fails to read the time value but I notice it does read datetimes from some of the example databases so it clearly can work.
Can anyone tell me what I'm missing.
Thanks
Trying to construct a SQL statement as a string in Delphi code can be a bit error prone. However, you should find that the following code executes correctly
procedure TForm2.btnInsertRowClick(Sender: TObject);
const
sInsertRow = ' INSERT INTO Table1 (Name,time) VALUES(''Fred'',datetime(''now''))';
begin
FDConnection1.ExecSql(sInsertRow);
end;
Btw, in general it would be better to use a parameterised INSERT statement than a literal one like the above, but it's not really practical here as you are inserting the now value returned by Sqlite rather than the Delphi now
function.
**Update: ** The code below is from a minimal project which creates your example table,
inserts a row into it and then presents it in db-aware controls (DBGrid, DBEdit) for editing.
It all works exactly as it ought to. In particular, any changes to the row data made through
those controls are retained when tha app is next run. Note that the SQL which creates the table specifies a primary key on the Name column/field: this is necessary for the FDQuery1 to generate the UPDATE statements needed to save changes back to the on-disk table.
The code in the btnSelectClick handler shows how to set the time field's DisplayFormat and EditMask properties so that it only displays the time part of the stored DateTime data.
type
TForm2 = class(TForm)
FDConnection1: TFDConnection;
FDQuery1: TFDQuery;
DBGrid1: TDBGrid;
DBNavigator1: TDBNavigator;
DataSource1: TDataSource;
DBEdit1: TDBEdit;
btnCreateTable: TButton;
btnInsertRow: TButton;
btnSelect: TButton;
[...]
public
[...]
const
sCreateTable = 'CREATE TABLE ''TABLE1'' (''Name'' VarChar(16) primary key, ''Time'' datetime)';
sInsertRow = ' INSERT INTO Table1 (Name,time) VALUES(''Fred'',datetime(''now''))';
sSelect = 'select * from table1';
procedure TForm2.btnCreateTableClick(Sender: TObject);
begin
FDConnection1.Connected := True;
FDConnection1.ExecSql(sCreateTable);
end;
procedure TForm2.btnInsertRowClick(Sender: TObject);
const
begin
FDConnection1.ExecSql(sInsertRow);
end;
procedure TForm2.btnSelectClick(Sender: TObject);
var
AField : TDateTimeField;
begin
FDQuery1.SQL.Text := sSelect;
FDQuery1.Open;
// The following shows how to control how the time field is formatted for display
// in gui controls using the field's DisplayFormat
// and how to set up its EditMask for editing
AField := FDQuery1.FieldByName('time') as TDateTimeField;
AField.DisplayFormat := 'hh:nn:ss';
AField.EditMask := '!90:00:00;1;_';
end;
end.
.DFM file
object Form2: TForm2
[...]
object FDConnection1: TFDConnection
Params.Strings = (
'Database=D:\Delphi\Code\FireDAC\db1.sqlite'
'DriverID=SQLite')
LoginPrompt = False
end
object FDQuery1: TFDQuery
Connection = FDConnection1
end
object DataSource1: TDataSource
DataSet = FDQuery1
end
object DBGrid1: TDBGrid
DataSource = DataSource1
end
object DBNavigator1: TDBNavigator
DataSource = DataSource1
end
object DBEdit1: TDBEdit
DataField = 'Time'
DataSource = DataSource1
end
end

Why does FireDAC ignore index name?

I'm trying to creating a table in a SQL Server database using FireDAC. However, instead of using the index name I provide, FireDAC uses a bad index name, raising an exception and the table does not get created. Am I doing something wrong? If not, is there a work-around?
Note that I'm using the valid database schema name cnf for TableName. I specifically need to create the table in a schema.
Simplest test case:
var
Connection: TFDConnection;
Table: TFDTable;
begin
Connection := TFDConnection.Create(nil);
Table := TFDTable.Create(nil);
try
Connection.Params.Add ('DriverID=MSSQL');
Connection.Params.Add ('OSAuthent=No');
Connection.Params.Add ('User_Name=sa');
Connection.Params.Add ('Password=XXXXXX');
Connection.Params.Add ('Server=DAVE-DELL\MSSQLSERVER2016');
Connection.Params.Add ('Database=PROJECT_DB');
Connection.Params.Add ('MARS=No');
Connection.Open;
Table.Connection := Connection;
Table.TableName := 'cnf.TestTable';
Table.FieldDefs.Add ('TableID', ftAutoInc, 0, true);
Table.FieldDefs.Add ('Field1', ftInteger, 0, true);
Table.FieldDefs.Add ('Field2', ftstring, 100, true);
Table.IndexDefs.Add ('PK_XYZ', 'TableID', [ixPrimary]); // should use this index name!
Table.CreateTable (true);
finally
Table.Free;
Connection.Free;
end;
end;
An exception is raised:
[FireDAC][Phys][ODBC][Microsoft][SQL Server Native Client 11.0][SQL Server]Incorrect syntax near '.'.
Running SQL Server Profiler shows me that FireDAC is trying to create the index using the following SQL code:
ALTER TABLE temp.TestTable ADD CONSTRAINT [cnf].[PK_TestTable] PRIMARY KEY (TableID)
And, of course, [cnf].[PK_TestTable] is not a valid index name in T-SQL, which is the crux of the problem.
If I remove the line Table.IndexDefs.Add, the table is created properly, but without the index.
If I replace that line with the following, it gives the same problem:
with Table.IndexDefs.AddIndexDef do begin
Name := 'PK_XYZ';
Options := [ixPrimary];
Fields := 'TableID';
end;
If I replace setting the table name with the following, it gives the same problem:
Table.TableName := 'TestTable';
Table.SchemaName := 'cnf';
Why is it using it's own (wrong) index name, instead of the name I gave it? (i.e. PK_XYZ)
Embarcadero® Delphi 10.1 Berlin Version 24.0.25048.9432
SQL Server 2016 (SP2-CU4) - 13.0.5233.0 (X64)
Am I doing something wrong?
Why is it using it's own (wrong) index name, instead of the name I gave it?
You seem to be doing everything just right. The issue is with the generated SQL command as you have tracked that down. SQL Server doesn't allow schema name in constraint name when adding a constraint using ALTER TABLE. Constraints created this way automatically become part of schema of the related table, however you should later use schema name when referring to the constraint:
SELECT OBJECT_ID('cnf.PK_XYZ')
Now where do the things go wrong? FireDAC uses TFDPhysCommandGenerator and its ancestors to generate SQL commands for specific DBMS. Your call to CreateTable method results in call to TFDPhysCommandGenerator.GetCreatePrimaryKey, which is responsible for generating SQL for primary key. It also contains this code:
sTab := GetFrom;
FConnMeta.DecodeObjName(sTab, rName, nil, [doUnquote]);
rName.FObject := 'PK_' + rName.FObject;
Result := 'ALTER TABLE ' + sTab + ' ADD CONSTRAINT ' +
FConnMeta.EncodeObjName(rName, nil, [eoQuote, eoNormalize]) + ' PRIMARY KEY (';
What this code does is that it takes your fully qualified table name (sTab) splits it (DecodeObjName) into parts (rName) prepends 'PK_' to table name and joins the parts (EncodeObjName) back to fully qualified name, which is then used as the constraint name for your primary key. Now we can clearly see that command generator ignores your index name and generates erroneous T-SQL. This can either be a bug or just a not supported feature. EMBT has to make decision on that. I'd recommend reporting it as a bug.
Is there a work-around?
Yes, you can either hook problematic method or you can override it in your own derived class. Implementation none of these is trivial and due to legal issues I'm not going to extend it here, because I would have to duplicate the original FireDAC code.
As for the syntax error adding these lines to 'TFDPhysCommandGenerator.GetCreatePrimaryKey' implementation after DecodeObjName would fix the issue:
rName.FCatalog := '';
rName.FSchema := '';
rName.FBaseObject := '';
rName.FLink := '';
Fixing constraint name is going to be more cumbersome than that, because the method only receives index column names as argument and has no obvious access to original IndexDefs where you could just use index name as primary key constraint name. Gaining access to index name from there would also allow you to get rid of decoding/encoding table name into index name. This process, however, could be essential for other DMBS's than SQL Server.
PS: If only half of all the questions were written in this manner ... Thank you for this wonderful question.

DBMS_METADATA: Get table SXML with triggers

I want to read the SXML representation of a table using this simple function:
CREATE OR REPLACE FUNCTION get_table_sxml(name IN VARCHAR2) RETURN CLOB IS
open_handle NUMBER;
transform_handle NUMBER;
doc CLOB;
BEGIN
open_handle := DBMS_METADATA.OPEN('TABLE');
DBMS_METADATA.SET_FILTER(open_handle,'NAME',name);
transform_handle := DBMS_METADATA.ADD_TRANSFORM(open_handle, 'SXML');
dbms_metadata.set_transform_param(transform_handle,'REF_CONSTRAINTS', true);
dbms_metadata.set_transform_param(transform_handle,'CONSTRAINTS', true);
doc := DBMS_METADATA.FETCH_CLOB(open_handle);
DBMS_METADATA.CLOSE(open_handle);
RETURN doc;
END;
When i select the generated XML with "SELECT get_table_sxml('TABLENAME') FROM dual" i get the complete xml representation, including constraints and ref constraints.
However the triggers associated with this table are missing in the output.
Can anyone give me a hint what i have to do to get the triggers into the output-xml?
The resulting XML is supposed to be used on another database to compare the tables and build diff-scripts using the DBMS_METADATA-package. So i need to use the "sxml" format.

How to change Clientdataset field datatype at runtime

For Delphi ClientDataSets where fields have been define at design time, is there a way at runtime to change a specific field's datatype ( change the cds.Fields[n].DataType) ?
I have a legacy Delphi 7 program with both SQLDataSet and ClientDataSet fields set at design time (in order to override various properties).
These are connected to a 3rd-party Sybase SQL Anywhere 11 database.
Recently the vendor changed all 'Description' fields from VarChar(128) to long varchar, but only for certain of his customers. So, my code has to support both types of fields when I query on these 'Description' fields.
I was hoping to set conditional compilation on the class field types (then add the fields before opening the SQL/CLient Dataset), but the compiler ignores {$IFDEF } conditionals in the component definition section of the class (which, the more I think about it, makes good sense)!
There are dozens of modules, with hundreds of fields affected, so any insight is appreciated.
I have also faced this problem before, not with CDS, but with TADODataSet using persistent fields at design time. I think that the code below will help you get the idea of how to fix/patch your CDS datasets.
The idea is to query the relevant table schema; get the actual fileds data type; and "change" the persistent field type by un-attaching it from the DataSet and adding a new matching persistent filed instead:
// TData class
procedure TData.DataModuleCreate(Sender: TObject);
var
I: Integer;
begin
for I := 0 to ComponentCount - 1 do
if (Components[I] is TCustomADODataSet) then
DataSetPrepareMemoFields(TDataSet(Components[I]));
end;
procedure TData.DataSetPrepareMemoFields(DataSet: TDataSet);
var
Fld: TField;
I: Integer;
FldName, CompName: string;
AOwner: TComponent;
begin
// Here you need to query the actual table schema from the database
// e.g. ADOConnection.GetFieldNames and act accordingly
// check which DataSet you need to change
// if (DataSet = dsOrders) or ... then...
if DataSet.FieldList.Count > 0 then
for I := DataSet.FieldList.Count - 1 downto 0 do
begin
if DataSet.FieldList.Fields[I].ClassNameIs('TMemoField') and (DataSet.FieldList.Fields[I].FieldKind = fkData) then
begin
// save TMemoField properties
AOwner := DataSet.FieldList[I].Owner;
CompName := DataSet.FieldList[I].Name;
FldName := DataSet.FieldList.Fields[I].FieldName;
// dispose of TMemoField
DataSet.FieldList[I].DataSet := nil; // Un-Attach it from the DataSet
// create TWideADOMemoField instead
Fld := TWideADOMemoField.Create(AOwner); // Create new persistent Filed instead
Fld.Name := CompName + '_W';
Fld.FieldName := FldName;
Fld.DataSet := DataSet;
end;
end;
end;
That said, After I have fixed that issue, I have never ever used persistent fields again.
All my fields are generated in run time. including calculated/lookup/internal fields.

Resources