How to execute function with parameter in PostgreSQL using anonymous code block in FireDAC? - sql

My database (PostgreSQL 11) has a function that must be called to execute an action. The return is not important, as long as there's no error. This function has arguments that must be passed as parameters.
I cannot directly use TFDConnection.ExecSQL because I use parameters of type array that is not supported by the method (to my knowledge). So, I use TFDQuery.ExecSQL like this:
msql1: = 'DO $$ BEGIN PERFORM DoIt (:id,:items); END $$; ';
{... create FDQuery (fq), set connection, set SQL.Text to msql1}
fq.Params.ParamByName ('id'). AsInteger: = 1;
{$ REGION 'items'}
fq.ParamByName ('items'). DataType: = ftArray;
fq.ParamByName ('items'). ArrayType: = atTable; // Must be atTable, not atArray
if length (items)> 0 then
begin
fq.ParamByName ('items'). ArraySize: = length (items);
for it: = 0 to length (items) -1 do
fq.ParamByName ('items'). AsIntegers [it]: = items [it];
end;
fq.ExecSQL;
{$ ENDREGION}
When executing the above code, the error above message raises
"Parameter 'id' not found".
After some research that suggested using fq.Params.ParamByName I was also unsuccessful.
However, if you change the way the function is called to
select DoIt (:id,:items); and obviously replacing the execution with fq.Open works perfectly.
Is it possible to execute a PL / pgSQL block that contains parameters in the function called by this block using TFDConnection / TFDQuery?
PS: I'm using Delphi Rio 10.3.3

When you assign a value to TFDQuery.SQL, FireDAC does some pre-processing of the SQL based on various options. ResourceOptions.CreateParams option controls whether parameters should be processed. This is enabled by default.
The preprocessor recognizes string literals in your SQL and doesn't try to look for the parameters in them. You used dollar quoted string constant and that's why FireDAC doesn't recognize parameters in it. Even if you add parameter manually I think that FireDAC would not bind the value.
With that said, the proper way to execute stored procedures/function is to use TFDStoredProc. You just assign StoredProcName and call its Prepare method which will retrieve procedure's metadata (parameters) from database so you don't need to set ArrayType or DataType of the parameter.
In your code you set the DataType to ftArray which is wrong, because in case of array parameter it should be set to array's element type. Anyway, by setting fq.ParamByName ('items').AsIntegers you effectively set parameter's DataType to ftInteger. All you need to do is to set ArraySize
Here's what you should do instead:
procedure DoIt(Connection: TFDConnection; ID: Integer; const Items: TArray<Integer>);
var
StoredProc: TFDStoredProc;
ParamItems: TFDParam;
Index: Integer;
begin
StoredProc := TFDStoredProc.Create(nil);
try
StoredProc.Connection := Connection;
StoredProc.StoredProcName := 'DoIt';
StoredProc.Prepare;
StoredProc.Params.ParamByName('id').AsInteger := ID;
if Length(Items) > 0 then
begin
ParamItems := StoredProc.Params.ParamByName('items');
ParamItems.ArraySize := Length(Items);
for Index := Low(Items) to High(Items) do
ParamItems.AsIntegers[Index] := Items[Index];
end;
StoredProc.ExecProc;
finally
StoredProc.Free;
end;
end;
Alternatively you can use ExecFunc to get the result of stored function.

Related

Adressing multiple, similar variables/objects in Delphi

I am writing a program that uses many shapes and I need to create a procedure to turn them all white. The shapes in question are named SectorBorder1 to SectorBorder20.
Is there a way to adress the shapes like this or similarly?
SectorBorder[X].brush.color := ClWhite;
Inc(X);
...where X is the number (obviously), instead of having to do:
SectorBorder1.brush.color := ClWhite;
SectorBorder2.brush.color := ClWhite;
...
SectorBorder20.brush.color := ClWhite;
So basically being able to differentiate names through a variable. This is the only way I could think of describing it. (Sorry, could someone also maybe include a better discription?) Any advice would be greatly apreciated.
Use an array
private
SectorBorders: array[1..20] of TShape;
procedure TMyForm.FormCreate(Sender: TObject):
begin
SectorBorders[1] := SectorBorder1;
..
SectorBorders[20] := SectorBorder20;
end;
procedure TMyForm.SetAllToWhite;
var
X: Integer;
begin
for X := Low(SectorBorders) to High(SectorBorders) do
SectorBorders[X].Brush.Color := clWhite;
end;
As an alternative to an array you can use a List. If you use the TList from System.Generics.Containers you can easily pass through all elements of the list.
You use it like this:
interface
uses System.Generics.Collections; // you need this to get the TList, you also need your other refereces of course
...
protected
pShapes: TList<TShape>;
procedure TMyForm.FormCreate(Sender: TObject):
var
nLoop: Integer;
begin
Self.pShapes:=TList<TShape>.Create;
nLoop:=0;
while(nLoop<Self.ComponentCount) do
begin
if(Self.Components[nLoop] is TShape) then
pList.Add(TShape(Self.Components[nLoop]));
Inc(nLoop);
end;
end;
procedure TMyForm.SetAllToWhite;
var
pShape: TShape;
begin
for pShape in Self.pShapes do
pShape.Brush.Color := clWhite;
end;
Choosing whether you want to use an array or a TList will be partially down to preference, but also down to what else you may want to do with the collection of TShapes and how they are managed within the object.
You can use the FindComponent method of the form.
shape:= FindComponent('SectorBorder' + IntToStr(i)) as TShape;
if shape <> nil then
pShape.Brush.Color := clWhite;
See http://docwiki.embarcadero.com/CodeExamples/Sydney/en/FindComponent_(Delphi)

cannot perform this operation on a closed dataset and incorrect data entered into table

I have been getting this error and I am struggling to find what I have done wrong. What doesn't make sense is that the result is still posted to the database with all fields being correct except the name field. In the name field it enters 'dmInfo' for some reason.
with dmQuery_u.dmInfo do
begin
dsInfo.Edit;
qryData.SQL.Clear;
qryData.SQL.Add('SELECT * FROM eventinfo ORDER BY eventnumber');
qryData.Open;
qryData.Last;
autonum := qryData['eventnumber'] + 1;
qryData.SQL.Clear;
qryData.SQL.Add(
'INSERT INTO eventinfo (eventnumber, bandname, venue, dateofevent, ticketcost, openingact, amountbooked)');
qryData.SQL.Add(
'VALUES (:eventnumber, :bandname, :venue, :dateofevent, :ticketcost, :openingact, :amountbooked)');
qryData.Parameters.ParamByName('eventnumber').Value := autonum;
qryData.Parameters.ParamByName('bandname').Value := name;
qryData.Parameters.ParamByName('venue').Value := venue;
qryData.Parameters.ParamByName('dateofevent').Value := date;
qryData.Parameters.ParamByName('ticketcost').Value := ticketcost;
qryData.Parameters.ParamByName('openingact').Value := openingact;
qryData.Parameters.ParamByName('amountbooked').Value := amountbooked;
qryData.ExecSQL;
qryData.SQL.Clear;
qryData.SQL.Add('SELECT * FROM eventinfo');
qryData.Last;
qryData.Open;
Your error is being caused here :
qryData.ExecSQL;
qryData.SQL.Clear;
qryData.SQL.Add('SELECT * FROM eventinfo');
qryData.Last; { !! should be after ---v }
qryData.Open; { should be first ---^ }
Calling .ExecSQL executes a non-query (ie : returns no recordset) command. Since there is no valid dataset here, calling .Last will throw an error. You have to call .Open first.
For the second issue of your name field populating with dmInfo, you're being caught out by use of the dangerous with statement here:
with dmQuery_u.dmInfo do
{...}
qryData.Parameters.ParamByName('bandname').Value := name;
Here the with statement is hiding the name variable you want to use and is instead interpreting it as dmQuery_u.dmInfo.Name. Either get rid of the with statement and use explicit qualifiers for everything or change the name of your name variable.

Delphi 2007 Adoquery Parameters not working

ADOQuerySelect.Close;
ADOQuerySelect.SQL.Add(' AND (дата_заказа between #'+dat+'# and #'+da+'#)');
if ComboBox6.Text <> '' then
begin
ADOQuerySelect.Parameters.ParamByName('Name').Value := ComboBox6.Text ;
ADOQuerySelect.SQL.Add(' AND (Наименование = :Name)');
end;
ADOQuerySelect.Open;
I use Delphi 2007, MS Access. And i dont now how work with parameters. On this code, i have error: parameter Name not found. I tried many other variants of code, but they all not working. I add parameter Name via GUI with datatype ftstring;
Changing SQL in your code clears the existing parameters, so there's no parameter called 'Name' at the time you try to set its value. You then create the Name parameter, but it's too late.
Change the order of your statements:
ADOQuerySelect.Close;
ADOQuerySelect.SQL.Add(' AND (дата_заказа between #'+dat+'# and #'+da+'#)');
if ComboBox6.Text <> '' then
begin
ADOQuerySelect.SQL.Add(' AND (Наименование = :Name)');
ADOQuerySelect.Parameters.ParamByName('Name').Value := ComboBox6.Text ;
end;
ADOQuerySelect.Open;
You should be using parameters for all your replacements, BTW. Let the database driver handle conversions and date formats and quoting values and all of that for you.
In the object inspector ADOQuerySelect should have 'Name' among list of parameters.
You can also use the following code to create the parameter 'Name':
with ADOQuerySelect.Parameters.AddParameter do
begin
Name := 'Name';
DataType := ftString;
end;

Adding Many (UDFs) Validation Functions to Oracle - Which Method Run Fastest

I have to move around 50+ validation functions into Oracle. I'm looking for the approach that runs fastest, but also would like to get around a boolean issue if possible. The return object for them all needs to be the same so that the application can react off the result in a consistent fashion and alert the user or display whatever popups, messages we may need. I created a valObj for this, but not sure yet if that is the best approach. The return format can be changed because the front-end that reacts off of it is not developed yet. In the end it will contain many different validation functions, from integer, number, phone, email, IPv4, IPv6, etc... This is what I have so far...
/***
This is the validation object.
It stores 1 for valid, 0 for not valid and some helper text that can be relayed back to the user.
***/
create or replace type valObj as object (
result number(1),
resultText varchar(32000)
);
/***
Coming from ColdFusion this seems clean to me but the function
will end up being a couple thousand lines long.
***/
create or replace function isValid(v in varchar2, format in varchar2)
return valObj
is
test number;
begin
if format = 'number' then
begin
test := to_number(v);
return valObj(1,null);
exception when VALUE_ERROR then return valObj(0,'Invalid number. Valid formats are: 12345, 12345.67, -12345, etc...');
end;
elsif format = 'integer' then
null; --TO DO
elsif format = 'email' then
null; --TO DO
elsif format = 'IPv4' then
null; --TO DO
elsif format = 'IPv6' then
null; --TO DO
end if;
--dozens of others to follow....
end;
/
/* Example Usage in SQL */
select isValid('blah','number') from dual; -- returns: (0, Invalid number. Valid formats are: 12345, 12345.67, -12345, etc...)
select isValid('blah','number').result from dual; -- returns: 0
select isValid('blah','number').resulttext from dual; -- returns: Valid formats are: 12345, 12345.67, -12345, etc...
select isValid(1234567890.123,'number') from dual; -- returns: 1,{null}
select isValid(1234567890.123,'number').result from dual; -- returns: 1
select isValid(1234567890.123,'number').resulttext from dual; -- returns: {null}
/* Example Usage in PL/SQL */
declare
temp valObj;
begin
temp := isValid('blah','number');
if (temp.result = 0) then
dbms_output.put_line(temp.resulttext);
else
dbms_output.put_line('Valid');
end if;
end;
/
My questions are:
When using it in PL/SQL I would love to be able to do boolean checks instead like this: if (temp.result) then but I can't figure out a way, cause that won't work in SQL. Should I just add a 3rd boolean attribute to the valObj or is there another way I don't know of?
These validation functions could end up being called within large loops. Knowing that, is this the most efficient way to accomplish these validations?
I'd appreciate any help. Thanks!
UPDATE: I forgot about MEMBER FUNCTIONS. Thanks #Brian McGinity for reminding me. So I'd like to go with this method since it keeps the type and its functions encapsulated together. Would there be any speed difference between this method and a stand-alone function? Would this be compiled and stored the same as a stand-alone function?
create or replace type isValid as object (
result number(1),
resulttext varchar2(32000),
constructor function isValid(v varchar, format varchar) return self as result );
/
create or replace type body isValid as
constructor function isValid(v varchar, format varchar) return self as result as
test number;
begin
if format = 'number' then
begin
test := to_number(v);
self.result := 1;
self.resulttext := null;
return;
exception when VALUE_ERROR then
self.result := 0;
self.resulttext := 'Invalid number. Valid formats are: 12345, 12345.67, -12345, etc...';
return;
end;
elsif format = 'phone' then
null; --TO DO
end if;
--and many others...
end;
end;
/
/* Example Usage in SQL */
select isValid('a','number') from dual;
/* Example Usage in PL/SQL */
declare
begin
if (isValid('a','number').result = 1) then
null;
end if;
end;
/
TEST RESULTS:
/* Test isValid (the object member function), this took 7 seconds to run */
declare
begin
for i in 1 .. 2000000 loop
if (isValid('blah','number').result = 1) then
null;
end if;
end loop;
end;
/* Test isValid2 (the stand-alone function), this took 16 seconds to run */
declare
begin
for i in 1 .. 2000000 loop
if (isValid2('blah','number').result = 1) then
null;
end if;
end loop;
end;
Both isValid and isValid2 do the same exact code, they just run this line test := to_number(v); then do the exception if it fails and return the result. Does this appear to be a valid test? The Object member function method is actually faster than a stand-alone function???
The stand-alone function can be much faster if you set it to DETERMINISTIC and if the data is highly repetitive. On my machine this setting decreased run time from 9 seconds to 0.1 seconds. For reasons I don't understand that setting does not improve performance of the object function.
create or replace function isValid2(v in varchar2, format in varchar2)
return valObj
deterministic --<< Hit the turbo button!
is
test number;
begin
if format = 'number' then
begin
test := to_number(v);
return valObj(1,null);
exception when VALUE_ERROR then return valObj(0,'Invalid number. Valid formats are: 12345, 12345.67, -12345, etc...');
end;
end if;
end;
/
May also want to consider utilizing pls_integer over number. Don't know if it will buy you much, but documents suggest some gain will be had.
http://docs.oracle.com/cd/B10500_01/appdev.920/a96624/03_types.htm states,
"You use the PLS_INTEGER datatype to store signed integers. Its magnitude range is -2*31 .. 2*31. PLS_INTEGER values require less storage than NUMBER values. Also, PLS_INTEGER operations use machine arithmetic, so they are faster than NUMBER and BINARY_INTEGER operations, which use library arithmetic. For efficiency, use PLS_INTEGER for all calculations that fall within its magnitude range."

SQL Query fails on empty result

I have a function that performs a query on a SQL database with an ADO connection, it is simply designed to provide a single result for a database entry that can only have one match for a SELECT type of query (i.e. get me the x value from ID 45, where there is and can only be one ID 45 entry).
The function works fine, until I hit a query that returns no results. The query just hangs, and the application cannot continue. Here is an example query string:
'SELECT Cost FROM MaterialCost ' +
'WHERE MaterialType = ''' + 'MS' +
''' AND Thickness = ''' + '0.250' + '''';
Again this exact string will work fine until I maybe query for something that I know before hand doesn't exist, which should return null or an empty string. Here is the function:
function SelectOneQuery(AQueryString : String; AADOConnectionString : String) : String;
var
ADOQuery: TADOQuery;
begin
//Create empty ADO Query, then connect to connection string
ADOQuery := TADOQuery.Create(nil);
ADOQuery.ConnectionString:=AADOConnectionString;
ADOQuery.SQL.Clear;
ADOQuery.SQL.Add(AQueryString);
ADOQuery.ExecSQL;
ADOQuery.Open;
//Set first query result and return first result
ADOQuery.First;
if(ADOQuery.Fields.Count > 0) then begin
result:=ADOQuery.Fields[0].Value;
end
else begin
result := '';
end;
end;
I added the fields count thing, but I'm not sure if that's helping at all. Basically, if there are no result, i want result := ''
You have a few problems in your code snippet:
The main problem is that you are checking FieldCount. FieldCount will always be nonzero because it contains the number of columns your query returns, regardless of the fact your query returned records or not. One option is to check RecordCount which represents the number of rows returned but a better option is to check EOF flag.
you are leaking ADOQuery. Always use try/finally blocks to create and cleanup objects.
ExecSQL is used for queries that don't return recordsets (like INSERT and DELETE),
use Open instead
No need to use First after Open
If you use the same query over and over you are better off using parameters, as a bonus your code will be more readable.
Example:
ADOQuery.SQL.Text := 'SELECT Cost FROM MaterialCost WHERE MaterialType = :MaterialType AND Thickness = :Thickness';
ADOQuery.Parameters.ParamByname('MaterialType').Value := 'MS';
ADOQuery.Parameters.ParamByname('Thickness').Value := 0.25;
Your function code should be something like this:
function SelectOneQuery(const AQueryString, AADOConnectionString: string): string;
var
ADOQuery: TADOQuery;
begin
Result := '';
ADOQuery := TADOQuery.Create(nil);
try
ADOQuery.ConnectionString := AADOConnectionString;
ADOQuery.SQL.Text := AQueryString;
ADOQuery.Open;
if not ADOQuery.EOF then
Result := ADOQuery.Fields[0].AsString;
finally
ADOQuery.Free;
end;
end;