Delphi7 - OmniXML - select specific node with multiple parameters - delphi

XML file:
<Partner>
...
</Partner>
<Partner>
<K1>10</K1>
<K2>3</K2>
<K3>5254304</K3>
<K4>test name</K4>
<K5>637.51</K5>
<K6>159.38</K6>
<K7>802.39</K7>
<K8>0.00</K8>
<K9>802.39</K9>
<Invoices>
<Invoice>
<R1>1</R1>
<R2>4-02R0113-12</R2>
<R3>2014-12-29</R3>
<R4>2014-12-29</R4>
<R5>398</R5>
<R6>637.51</R6>
<R7>159.38</R7>
<R8>802.39</R8>
<R9>0.00</R9>
<R10>802.39</R10>
</Invoice>
</Invoices>
</Partner>
<Partner>
...
</Partner>
In my XML file I have repeating nodes <Partner>. Every partner has its own identification number written in node <K3>.
Every partner can have multiple invoices.
I need to find and read values in <R6> and <R7> in the invoice where
I know values for <R2>, <R3>, <R8>.
How do I search for specific Invoice where search criteria are multiple fields <R2>, <R3>, <R8> of partner where search criteria is field <K3> and get field values for <R6> and <R7>?
How to add multi criteria to SelectSingleNode?
My code:
procedure TfrmTest.TestReadOmniXML;
var
xml: IXMLDocument;
iNodePartner, iNodePartnerInvoice, iNodePartnerInvoiceR6, iNodePartnerInvoiceR7 : IXMLNode;
begin
xml := CreateXMLDoc;
xml.Load('c:\test.xml');
iNodePartner := XML.SelectSingleNode('//Partner[K3=' + '0254304' + ']');
iNodePartnerInvoice := iNodePartner.SelectSingleNode(
//single query works OK
'.//Racuni/Racun[R2=' + '4-02R0113-12' + ']'
//but I need to add these fields also
// ' and [R3=' + '2014-12-29' + ']' +
// ' and [R8=' + '802.39' + ']'
);
if Assigned( iNodePartnerInvoice ) then
begin
iNodePartnerInvoiceR6 := iNodePartnerInvoice.SelectSingleNode('./R6');
Label1.Caption := iNodePartnerInvoiceR6.Text;
iNodePartnerInvoiceR7 := iNodePartnerInvoice.SelectSingleNode('./R7');
Label2.Caption := iNodePartnerInvoiceR7.Text;
end;
...
end;

OmniXMLXPath doesn't support that out of the box. I have, however, added support for consecutive filters last year, but forgot to push that change to the public site :(
With the updated version, this will work:
program Project29;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils,
OmniXML,
OmniXMLXpath,
OmniXMLUtils;
var
xml: IXMLDocument;
node: IXMLNode;
begin
xml := CreateXMLDoc;
if XMLLoadFromFile(xml, 'c:\0\partner.xml') then begin
node := xml.SelectSingleNode('//Partner/Invoices/Invoice[R2="4-02R0113-12"][R3="2014-12-29"][R8="802.39"]');
if not assigned(node) then
Writeln('not found')
else begin
Writeln('R6=', GetNodeTextStr(node, 'R6', ''));
Writeln('R7=', GetNodeTextStr(node, 'R7', ''));
end;
end;
Readln;
end.
For now, you can get the fresh OmniXMLXPath.pas here: https://www.dropbox.com/s/nnvrz6wnmnpmxzn/OmniXMLXPath.pas

Given the example above, you can do something along the lines of:
//Partner[K2=3]/Invoices/Invoice[R1=1 and R5=398]
This gets the Ivoices of partner with K2=3 where ivoices have specific values for r1 and r5. Then, depending on how you need to process the invoices that match these criteria (e.g. if there is more than one invoice) you can then use an XPath expression (like sum or something else) to return a single value calculated from these Invoice elements.

Accordingly OmniXMLXPath.pas file, OmniXML does not support logical operation in XPath. So you can't search by 2+ attributes simultaneously.
You can select multiple nodes by calling SelectNodes('//Partner[K3=5254304]/Invoices/Invoice[R2='4-02R0113-12']) and check nodes by another attributes.
Or use MSXML parser (OmniXML_MSXML unit) if you can. This will work on your demo data:
procedure TForm1.Button1Click(Sender: TObject);
var
xml: IXMLDocument;
iNodePartner: IXMLNode;
begin
xml := CreateXMLDoc;
xml.Load('c:\test.xml');
iNodePartner := XML.selectSingleNode('//Partner[K3=5254304]/Invoices/Invoice[R2=''4-02R0113-12'' and R8=''802.39'']');
if iNodePartner = nil then
MessageDlg('Not found',mterror,[mbok],0);
end;

Related

FireDAC DBMS Identifiers for conditional statement

I'm trying to use the FireDAC DBMS Identifiers for generating a database specific query.
I'm connecting to a MySQL-Server currently (DriverID = MySQL). I want to query either from mysql or mssql with a limit / top query.
My current statement looks like this:
SELECT
{IF MSSQL} TOP(1) {fi} `tr`.`TaxRate_Primkey`
FROM
`tbl_taxrates` AS `tr`
WHERE
`tr`.`TaxRate_TaxCodeId` = `tc`.`TaxCode_Primkey`
AND
`tr`.`TaxRate_ValidSince` <= :DATE
ORDER BY `tr`.`TaxRate_ValidSince` DESC
{IF MySQL} LIMIT 1 {fi}
Of course I'm aware, that the escaping will not be correct for mssql, but that's a different story.
When I inspect the FireDAC Monitor the preprocessed query looks like this:
SELECT
TOP(1) `tr`.`TaxRate_Primkey`
FROM
`tbl_taxrates` AS `tr`
WHERE
`tr`.`TaxRate_TaxCodeId` = `tc`.`TaxCode_Primkey`
AND
`tr`.`TaxRate_ValidSince` <= ?
ORDER BY
`tr`.`TaxRate_ValidSince` DESC
LIMIT 1
Of course this will result in error
You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '.`TaxRate_Primkey` FROM `tbl_taxrates` AS `tr` WHERE `tr`.`TaxRate_TaxCodeId` ' at line 1 [errno=1064, sqlstate="42000"]
since it should only add the LIMIT 1 and omit the TOP(1).
Since I only have the Delphi Community Edition 10.4, I don't have access to the MSSQL-Driver. I thought, that could cause this error and I tried with others. But also with {if FIREBIRD} and {if ADS} it was the same.
The constructor looks like this:
constructor TFireDACTenantRepository.Create(
ADBName: string;
ADBServer: string;
APort: Integer;
AUserName: string;
APassword: string;
ALogger: TLogger
);
var
oParams: TStrings;
begin
inherited Create;
FLogger := ALogger;
Self.MonitorLink := nil;
Self.MonitorBy := mbRemote;
Self.Tracing := True;
FConnection := TFDConnection.Create(nil);
oParams := TStringList.Create;
try
oParams.Add('Server=' + ADBServer);
oParams.Add('Port=' + IntToStr(APort));
oParams.Add('Database=' + ADBName);
oParams.Add('User_Name=' + AUserName);
oParams.Add('Password=' + APassword);
oParams.Add('OSAuthent=No');
FDManager.AddConnectionDef('MySQLConnectionTenant', 'MySQL', oParams);
FConnection.Params.MonitorBy := Self.MonitorBy;
FConnection.ConnectionDefName := 'MySQLConnectionTenant';
FConnection.ResourceOptions.ParamCreate := True;
FConnection.ResourceOptions.MacroCreate := True;
FConnection.ResourceOptions.ParamExpand := True;
FConnection.ResourceOptions.MacroExpand := True;
FConnection.ResourceOptions.PreprocessCmdText := True;
FConnection.ResourceOptions.EscapeExpand := True;
finally
oParams.Free;
end;
FConnection.AfterConnect := DoAfterConnect;
FConnection.AfterDisconnect := DoAfterDisconnect;
end;
My question looks a bit related to this question here
How do I use FireDAC DBMS identifiers to conditionally change SQL text
Thanks
I found the solution. If you want to use the conditional escape sequence, you also have to use the FireDAC.Phys.XXX unit for all involved databases.
Since I want to use MSSQL, I have to add FireDAC.Phys.MSSQL to the use-clause.
The units are listed here:
https://docwiki.embarcadero.com/RADStudio/Sydney/en/Databases_(FireDAC)
Maybe this answer helps others.

What is the wildcards format for TEdit search function in FireDAC Delphi

This is my code for searching data using the TEdit component that triggers TFDQuery with parameter:
qryItems.ParamByName('searches').AsString := Format('%%%s%%',[edtSearch.Text]);
If I remove the wildcards (Format('%%%s%%')) format, it works. The wildcards will help me filter the query.
I like the code, its clean, simple, and straight forward. But, I am still not sure if it is correct — it is not returning anything!
My question is:
Does the code above works for query filtering from TEdit.OnChangeTracking event? Otherwise, what is the correct way of doing this?
UPDATE 1:
Heres the code from TFDQuery Editor:
SELECT category.name AS category, item.name, item.description
FROM item
JOIN category ON item.category_id = category.list_id
WHERE item.description LIKE :searches
ORDER BY item.sellable
LIMIT 100
Now, I am trying to access this from this code during runtime but it is not working:
qryItems.ParamByName('searches').AsString := Format('%%%s%%',[edtSearch.Text]);
I think the culprit here is this code Format('%%%s%%',[edtSearch.Text]), I am not getting this right.
A short answer is that you want to end up with a parameter assignment like this:
FDQuery1.Params[0].AsString := '%a%';
FDQuery1.Open();
assuming the value you want to match in your LIKE expression is simply the letter a. Or, if you want to use Format, you could do something like this:
FDQuery1.Params[0].AsString := Format('%%%s%%', [edFilter.Text]);
The reason for the three hash-signs in a row is that the first one 'escapes' the second one in the expression Format evaluates, and the third one, immediately before the 's' combines with it to act as the placeholder for a string as Format constructs its result.
However, given that you are not completely familiar with working with datasets and filtering,
I think you are making this unnecessarily difficult for yourself in at least two respects:
FMX + LiveBindings is not entirely bug free and has some quirks which may well get in your way.
The syntax for using the LIKE operator, which uses hash-signs (#), clashes with the use
of hash signs for resolving parameters in the Format function. This, in particular, can be
extremely confusing, especially when you are trying to obtain a syntactically valid
LIKE expression, whether it is for inclusion in the Sql your query uses or in a 'local filter,
i.e. one which uses the Filter + Filtered properties of the FDQuery.
So, I am going to make a suggestion which might possibly be unwelcome initially,
which is to do your exploration
of things like filtering in a VCL application such as the one below. It will only take a few minutes to set up,
but will probably save you some time and wear and tear on the nervous system compared with
trying to get it right in an FMX + LiveBinding application which is under development. Here is how:
Create a new VCL application and add these components to it.
FDConnection1: TFDConnection;
FDGUIxWaitCursor1: TFDGUIxWaitCursor;
FDQuery1: TFDQuery;
DBGrid1: TDBGrid;
DBNavigator1: TDBNavigator;
DataSource1: TDataSource;
edFilter: TEdit;
btnLocalFilter: TButton;
btnSqlFilter: TButton;
Add the code below to the form's file.
Put a debugger breakpoint on the line
case FilterMode of
and exlore the app's behaviour changing the contents of the edFilter control
and clicking the two buttons, once you've adapted the code to the data you have
available. Mine uses an Author's table, I can't remember where I got it from
but maybe it was from the Pubs sample database for Sql-Server.
The app shows - as I'm sure you've gathered - that you can filter the data displayed
by your app either server-side by changing the Sql used to retrieve the data or client-side by using
the Filter property of the FDQuery. So that you can easily see what's going on, the Sql for server-side
filtering is constructed by concatenating the contents of edFilter.Text with the
rest of the Sql, but in real life, you should never do that because of
its exposure to the Sql Injection exploit.
Code
type
TFilterMode = (fmLocal, fmSql);
type
TForm1 = class(TForm)
[...]
public
{ Public declarations }
FilterMode : TFilterMode;
end;
[...]
const
sOrderBy = ' order by lastname, forename';
sSql = 'select * from authors';
sFilteredSql = sSql + ' where lastname like :lastname%';
sLocalFilter = 'lastname like ''%%s%%''';
procedure TForm1.OpenFDQuery;
var
S : String;
begin
if FDQuery1.Active then FDQuery1.Close;
FDQuery1.Params.Clear;
FDQuery1.Filter := '';
FDQuery1.Filtered := True;
case FilterMode of
fmSql : begin
FDQuery1.Sql.Text := '';
// WARNING - don't do this for real - risk of Sql Injection exploit
// use a parameterised query instead - see http://docwiki.embarcadero.com/RADStudio/Rio/en/Using_Parameters_in_Queries
S := 'select * from authors where lastname like ''%' + edFilter.Text + '%''';
FDQuery1.Sql.Text := S;
end;
fmLocal : begin
FDQuery1.Sql.Text := sSql + sOrderBy;
S := 'lastname like ''%' + edFilter.Text + '%''';
FDQuery1.Filter := S;
FDQuery1.Filtered := True;
end;
end;
FDQuery1.Open;
end;
procedure TForm1.ApplySqlFilter;
begin
FilterMode := fmLocal;
OpenFDQuery;
end;
procedure TForm1.ApplyLocalFilter;
begin
FilterMode := fmLocal;
OpenFDQuery;
end;
procedure TForm1.btnLocalFilterClick(Sender: TObject);
begin
ApplyLocalFilter;
end;
procedure TForm1.btnSqlFilterClick(Sender: TObject);
begin
ApplySqlFilter;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
edFilter.Text := 'a';
end;

How to correctly use IFileOperation in Delphi to delete the files in a folder

I'm trying to create a simple example of using IFileOperation to delete the files in a
given directory, to include in the answer to another q for comparison with other methods.
Below is the code of my MRE. It
successfully creates 1000 files in a subdirectory off C:\Temp and then attempts to delete
them in the DeleteFiles method. This supposedly "easy" task fails but I'm not sure
exactly where it comes off the rails. The comments in the code show what I'm expecting
and the actual results. On one occasion, instead of the exception noted, I got a pop-up
asking for confirmation to delete an item with an odd name which was evidently an array of
numbers referring to a shell item, but my attempt to capture it using Ctrl-C failed;
I'm fairly sure I'm either missing a step or two, misusing the interfaces involved
or both. My q is, could anybody please show the necessary corrections to the code to get IFileOperation.DeleteItems() to delete the files in question, as I am completely out of my depth with this stuff? I am not interested in alternative methods of deleting these files, using the shell interfaces or otherwise.
procedure TForm2.DeleteFiles;
var
iFileOp: IFileOperation;
iIDList : ItemIDList;
iItemArray : IShellItemArray;
iArray : Array[0..1] of ItemIDList;
Count : DWord;
begin
iFileOp := CreateComObject(CLSID_FileOperation) as IFileOperation;
iIDList := ILCreateFromPath(sPath)^;
// IFileOperation.DeleteItems seems to require am IShellItemArray, so the following attempts
// to create one
// The definition of SHCreateShellItemArrayFromIDLists
// seems to require a a zero-terminated array of ItemIDLists so the next steps
// attempt to create one
ZeroMemory(#iArray, SizeOf(iArray));
iArray[0] := iIDList;
OleCheck(SHCreateShellItemArrayFromIDLists(1, #iArray, iItemArray));
// Next test the number of items in iItemArray, which I'm expecting to be 1000
// seeing as the CreateFiles routine creats that many
OleCheck(iItemArray.GetCount(Count));
Caption := IntToStr(Count); // Duh, this shows Count to be 1, not the expected 1000
OleCheck(iFileOp.DeleteItems(iItemArray));
OleCheck( iFileOp.PerformOperations );
// Returns Exception 'No object for moniker'
end;
procedure TForm2.Button1Click(Sender: TObject);
begin
DeleteFiles;
end;
procedure CreateFiles;
var
i : Integer;
SL : TStringList;
FileName,
FileContent : String;
begin
SL := TStringList.Create;
try
if not (DirectoryExists(sPath)) then
MkDir(sPath);
SL.BeginUpdate;
for i := 0 to 999 do begin
FileName := Format('File%d.Txt', [i]);
FileContent := Format('content of file %s', [FileName]);
SL.Text := FileContent;
SL.SaveToFile(sPath + '\' + FileName);
end;
SL.EndUpdate;
finally
SL.Free;
end;
end;
procedure TForm2.FormCreate(Sender: TObject);
begin
CreateFiles;
end;
You are leaking the memory returned by ILCreateFromPath(), you need to call ILFree() when you are done using the returned PItemIDList.
Also, you should not be dereferencing the PItemIDList. SHCreateShellItemArrayFromIDLists() expects an array of PItemIDList pointers, but you are giving it an array of ItemIDList instances.
Try this instead:
procedure TForm2.DeleteFiles;
var
iFileOp: IFileOperation;
iIDList : PItemIDList;
iItemArray : IShellItemArray;
Count : DWord;
begin
iFileOp := CreateComObject(CLSID_FileOperation) as IFileOperation;
iIDList := ILCreateFromPath(sPath);
try
OleCheck(SHCreateShellItemArrayFromIDLists(1, #iIDList, iItemArray));
finally
ILFree(iIDList);
end;
// Next test the number of items in iItemArray, which I'm expecting to be 1000
// seeing as the CreateFiles routine creates that many
OleCheck(iItemArray.GetCount(Count));
Caption := IntToStr(Count); // Duh, this shows Count to be 1, not the expected 1000
OleCheck(iFileOp.DeleteItems(iItemArray));
OleCheck( iFileOp.PerformOperations );
// Returns Exception 'No object for moniker'
end;
That being said, even if this were working correctly, you are not creating an IShellItemArray containing 1000 IShellItems for the individual files. You are creating an IShellItemArray containing 1 IShellItem for the C:\Temp subdirectory itself.
Which is fine if your goal is to delete the whole folder. But in that case, I would suggest using SHCreateItemFromIDList() or SHCreateItemFromParsingName() instead, and then pass that IShellItem to IFileOperation.DeleteItem().
But, if your goal is to delete the individual files without deleting the subdirectory as well, then you will have to either:
get the IShellFolder interface for the subdirectory, then enumerate the relative PIDLs of its files using IShellFolder.EnumObjects(), and then pass the PIDLs in an array to SHCreateShellItemArray().
get the IShellFolder interface of the subdirectory, then query it for an IDataObject interface using IShellFolder.GetUIObjectOf(), and then use SHCreateShellItemArrayFromDataObject(), or just give the IDataObject directly to IFileOperation.DeleteItems().
get an IShellItem interface for the subdirectory, then query its IEnumShellItems interface using IShellItem.BindToHandler(), and then pass that directly to IFileOperation.DeleteItems().

Delphi, FastReport params

I have a problem with printing
procedure Sendparams(const Pparams,pparvalues :array of string);
begin
for I := 0 to Length(Pparams) - 1 do
begin
lpar_name:=Pparams[i];
lpar_val:=pparvalues[i] ;
FfrxReport.Variables.AddVariable('Bez', lpar_name, lpar_val);
end;
Sendparams(['buyer','delivery'], ['buyer address', 'delivery address']);
Everything works fine until I try to print report; it says: Expression expected on Memo2.
Memo1.memo = '[buyer]';
Memo2.memo = '[delivery]';
memo1 and memo2 all other properties are the same. Any suggestions?
There are different possible traps.
If you want to use Addvariable (instead of variables.add) the category, in your case Bez has to be defined in the report, otherwise the variables won't be add. **
The assignment of the variables within the report hast to look like Memo1.Lines.Text :=<buyer>;
You will have to quote the string values of the variables
Sendparams(['buyer','delivery'], [QuotedStr('buyer address'), QuotedStr('delivery address')]);
**
Another attempt could be something like this, to avoid open arrays of string (where count of names and values accidentally could differ), to avoid a hard reference to the report within Sendparams and to deal with variables which already could be defined within the report.
Function PrepareReport(Report:TfrxReport; Variables: TfrxVariables;
ReportName: String):Boolean;// -- other parameters
var
i,k:Integer;
begin
// ....... other initializations
if Assigned(Variables) then
for i := 0 to Variables.Count - 1 do
begin
k := Report.Variables.IndexOf(Variables.Items[i].Name);
if k > -1 then
Report.Variables.Items[k].Value := Variables.Items[i].Value
else
begin
with Report.Variables.Add do
begin
Name := Variables.Items[i].Name;
Value := Variables.Items[i].Value;
end;
end;
end;
end;

Need help with Delphi and ADOTable filtering

i m trying to build an invoice program that holds data in an Access db. i have some tedit s, buttons, one datasource, one adotable, one dbgrid and a popup menu. database format is accdb.
Problem: i want the program to filter records while user is typing. it might filter dbgrid or tedit, doesn t matter. i somehow found some code, for example:
Table1.FilterOptions:=[foCaseInsensitive];
Table1.Filter:='Filmadi='+QuotedStr(Edit1.Text+'*');
Table1.Filtered:=true;
the code above gives this error: Project Project1.exe raised exception class eoleexception with message: Item cannot be found in the collection corresponding to the requested name or ordinal
other examples give various errors.
sincerely
onur
Use LIKE operator in your filter:
procedure DoIncrementalFilter(Dataset: TDataSet; const FieldName, SearchTerm: string);
begin
Assert(Assigned(Dataset), 'No dataset is assigned');
if SearchTerm = '' then
Dataset.Filtered := False
else
begin
Dataset.Filter := FieldName + ' LIKE ' + QuotedStr(SearchTerm + '*');
Dataset.Filtered := True;
end;
end;
Example:
DoIncrementalFilter(ADOTable1, 'Filmadi', Edit1.Text);

Resources