If i have a SQL statement like below
SELECT * FROM myTable WHERE CID = :vCID AND DataType = :vDataType
And usually i use TQuery to get some data like below
aQuery.ParamByName('vCID').Value := '0025';
aQuery.ParamByName('vDataType').AsInteger := 1;
But how can i ignore the "CID" key to get a SQL like
SELECT * FROM myTable WHERE DataType = :vDataType
I've try the below synctax, but failed
aQuery.ParamByName('vCID').Value := '%';
aQuery.ParamByName('vDataType').AsInteger := 1;
Please help me out, thank you.
Change your Query to
SELECT * FROM myTable
WHERE CID = ISNULL(:vCID,CID) AND DataType = ISNULL(:vDataType,DataType)
or
SELECT * FROM myTable
WHERE COALESCE(CID,'') = COALESCE(:vCID,CID,'')
AND COALESCE(DataType,0) = COALESCE(:vDataType,DataType,0)
The second one would handle the case of NULL values in the table too.
The Parameter you don't want to use can be set to Unassigned
aQuery.ParamByName('vCID').Value := Unassigned; // <<
aQuery.ParamByName('vDataType').AsInteger := 1;
Since :vCid is NULL it will be evaluated as CID = CID
The best option is to simply use separate queries:
aQueryBoth.SQL.Text := 'SELECT * FROM myTable WHERE CID = :vCID AND DataType = :vDataType';
...
aQueryBoth.ParamByName('vCID').Value := '0025';
aQueryBoth.ParamByName('vDataType').AsInteger := 1;
aQueryDataType.SQL.Text := 'SELECT * FROM myTable WHERE DataType = :vDataType';
...
aQueryDataType.ParamByName('vDataType').AsInteger := 1;
Usual but somewhat verbose way is to introduce yet another parameter.
SELECT * FROM myTable
WHERE ( ( CID = :vCID ) OR ( :IgnoreCID <> 0 ))
AND ( DataType = :vDataType )
Then turning your queries into
aQuery.ParamByName('vCID').Value := '0025';
aQuery.ParamByName('IgnoreCID').AsInteger := 0;
aQuery.ParamByName('vDataType').AsInteger := 1;
or
aQuery.ParamByName('vCID').Value := Unassigned;
aQuery.ParamByName('IgnoreCID').AsInteger := 1;
aQuery.ParamByName('vDataType').AsInteger := 1;
If the server has decent SQL Optimizer, then it would figure out when the 1st parameter is worth checking or not.
Related
I select some rows in dbgrid and then have to pass set of values in parameter of stored procedure or query. I use Firebird 3. How to pass multiple values in a single parameter if number of values is not predefined? For example, for 3 values of good_id I get error:
conversion error from string "7802 ,8403 ,11461"
create or alter procedure sp_goods (id varchar(60))
returns (
good varchar(50),
good_id integer)
as
begin
for select good_id, good from goods where good_id in (:id)
into :good_id, :good
do suspend;
end
procedure Button1Click(Sender: TObject);
var
str : String;
i : Integer;
begin
Query1.Close;
Query1.SQL.Text := 'select * from sp_goods(:id) ';
with DBGridGoods do
begin
if SelectedRows.Count > 0 then
begin
str := '';
With DataSource.DataSet do
for i := 0 to SelectedRows.Count - 1 do
begin
GotoBookmark(SelectedRows.Items[i]) ;
str := str + FieldByName('good_id').asString + ', ';
end;
str := copy( str, 1, length( str ) - 2 );
end;
end;
Query1.Params[0].AsString:=str;
Query1.Open;
end;
If I call stored procedure in IBExpert
select * from sp_goods('8403')
It works, but
select * from sp_goods('8403','7802')
returns error
Input parameter mismatch for procedure sp_goods.
The same error occurs if I use query instead of stored procedure.
I tried to use array for values, but get empty dataset:
procedure Button1Click(Sender: TObject);
var
a: array of integer;
begin
Query1.Close;
Query1.SQL.Text := 'select * from sp_goods(:id) ';
setlength(a, 2);
a[0]:= 7802;
a[1]:=8403;
Query1.Params[0].Value:= a;
Query1.Open;
end;
There is no way to pass set of values into single parameter in Firebird.
In your example whole stored procedure is meaningless and it is simpler and faster to select all values at once into original grid using join. If you wish to get goods for selected items only and to put them into a separate grid the best way is to perform the query in your loop instead of gathering list of ids. If you prepare the query once (it is a common mistake to do prepare() call inside of the loop) it will be quite fast.
I have achieved this in two ways in the past. One is by using Dynamic Queries, which is not what you want to do, unless there is no other option.
The other way is by using this procedure. I am dragging this from my archives, and there will be other ways to achieve this more efficiently. I am providing it to show how to do it.
create or alter procedure "Split_Line"
( IP_NOTE VARCHAR (16000),
IP_SEP CHAR (1))
returns (
"Index" INTEGER,
"Line" VARCHAR (16000))
as
declare variable lLines varchar (16000);
declare variable lMax integer;
declare variable lPos integer;
begin
lMax = 16000;
lLines = ip_Note;
"Index" = 0;
while (lLines is not null)
do begin
"Line" = null;
lPos = null;
select "Result" from "Pos" (:Ip_Sep, :lLines) into :lPos;
if (lPos is null or lPos = 0)
then begin
/* Last line with no separator */
"Line" = lLines;
lLines = null;
end
else if (lPos = 1 and lLines = Ip_Sep)
then begin
/* Last char is a separator */
"Line" = '';
lLines = null;
end
else begin
/* Normal Case */
// "Line" = "SubStr" (:lLines, 1, :lPos-1);
// lLines = "SubStr" (:lLines, :lPos+1, :lMax);
"Line" = substring (:lLines from 1 for :lPos-1);
lLines = substring (:lLines from :lPos+1 for :lMax);
end
"Index" = "Index" + 1;
suspend;
end
end
You call it with a comma separated values in a string and the separator character (comma in this case). And it returns a table that you use.
Example of usage
select * from "Split_Line" ('x,a,cat', ',')
will return
Index
Line
1
x
2
a
3
cat
And you can use it in your case
create or alter procedure sp_goods (id varchar(60))
returns (
good varchar(50),
good_id integer)
as
begin
for select good_id, good from goods
where good_id in (select cast("Line" as numeric (18, 0))
from "Split_Line" (:id, ','))
into :good_id, :good
do suspend;
end
Supporting procedure to compile before Split_String
create or alter procedure "Pos"
( SUBSTR VARCHAR (100),
STR VARCHAR (16000))
returns ( "Result" INTEGER)
as
DECLARE VARIABLE SubStr2 VARCHAR(16256); /* 1 + SubStr-lenght + Str-length */
DECLARE VARIABLE Tmp VARCHAR(255);
BEGIN
IF (SubStr IS NULL OR Str IS NULL)
THEN BEGIN
"Result" = NULL;
suspend;
EXIT;
END
IF (SubStr = '' OR Str = '')
THEN BEGIN
"Result" = 0;
suspend;
EXIT;
END
SubStr2 = SubStr || '%';
Tmp = '';
"Result" = 1;
WHILE (Str NOT LIKE SubStr2 AND Str NOT LIKE Tmp)
DO BEGIN
SubStr2 = '_' || SubStr2;
Tmp = Tmp || '_';
"Result" = "Result" + 1;
END
IF (Str LIKE Tmp)
THEN "Result" = 0;
suspend;
END
And I have replaced my substr (from my user defined function library) to use the firebird substring in the Split_Line procedure.
Apologies for the quoted identifiers, always use Dialect 3. And the odd capitalisation was to support Crystal Reports which at that time would only work with uppercase procedures.
The other way to do it is to use Dynamic Queries.
create or alter procedure sp_goods (id varchar(60))
returns (
good varchar(50),
good_id integer)
as
declare lsql varchar (5000);
begin
lsql = 'select good_id, good from goods where good_id in (' || :id || ')';
for execute statement lsql
into :good_id, :good
do suspend;
end
Disadvantages
Normally, when a procedure is compiled, the queries are prepared at that time. So, execution is faster. With Dynamic Queries or Dynamic Sql, the query has to be prepared every time the procedure is executed.
Normally, when a procedure is compiled, the engine validates the table and fields etc. In this case the validation happens at execution time. So you have to be really careful how you construct your query.
Note - I havent had time to test it with a real table, but it compiles. (Its 3am, so I might check that tomorrow).
I wouldn't normally recommend this, but everything has a place.
I have a TOraQuery with SQL defined something like this
SELECT ML.ID, Ml.detail1, Ml.detail2
FROM MY_LIST ML
WHERE
ML.ID in (:My_IDS)
If I was to build this query on the fly, I'd naturally end up with something like this:
SELECT ML.ID, Ml.detail1, Ml.detail2
FROM MY_LIST ML
WHERE
ML.ID in (14001,14002,14003)
However, I'd like to pass in 14001,14002,14003 as a parameter.
myListQuery.Active := False;
myListQuery.ParamByName('My_IDS').AsString := '14001,14002,14003';
myListQuery.Active := True;
But of course that generates an ORA-01722: invalid number. Do I have any other option other than building up the query on the fly.
AFAIK, it is not possible directly.
You'll have to convert the list into a SQL list in plain text.
For instance:
function ListToText(const Args: array of string): string; overload;
var
i: integer;
begin
result := '(';
for i := 0 to high(Args) do
result := result+QuotedStr(Args[i])+',';
result[length(result)] := ')';
end;
function ListToText(const Args: array of integer): string; overload;
var
i: integer;
begin
result := '(';
for i := 0 to high(Args) do
result := result+IntToStr(Args[i])+',';
result[length(result)] := ')';
end;
To be used as such:
SQL.Text := 'select * from myTable where intKey in '+ListToText([1,2,3]);
SQL.Text := 'select * from myTable where stringKey in '+ListToText(['a','b','c']);
Or in your case:
myListQuery.SQL.Text := 'SELECT ML.ID, Ml.detail1, Ml.detail);
myListQuery.SQL.Add('FROM MY_LIST ML ');
myListQuery.SQL.Add('WHERE ');
myListQuery.SQL.Add('ML.ID in ') + ListToText([14001,14002,14003]);
You can do it but it requires some additional setup. Hopefully this works with your version of Oracle.
Create a table type
Create a function that converts your string to your table type
Use CAST in the subquery. Pass your value to the bind variable using the same thing you have in your code (i.e. ParamByName('').AsString).
create or replace type myTableType as table of varchar2 (255);
create or replace function in_list( p_string in varchar2 ) return myTableType as
l_string long default p_string || ',';
l_data myTableType := myTableType();
n number;
begin
loop
exit when l_string is null;
n := instr( l_string, ',' );
l_data.extend;
l_data(l_data.count) :=
ltrim( rtrim( substr( l_string, 1, n-1 ) ) );
l_string := substr( l_string, n+1 );
end loop;
return l_data;
end;
select * from THE ( select cast( in_list(:MY_BIND_VARIABLE) as mytableType ) from dual ) a
If this works for you, credit for the answer and example code goes to Tom Kyte from Oracle who runs asktom.com. https://asktom.oracle.com/pls/asktom/f?p=100:11:0%3a%3a%3a%3aP11_QUESTION_ID:210612357425
You can use a "Macro"
Not quite what I was looking for, but it's a hair closer to a parameter than building the SQL on the fly.
Create the TOraQuery like this
SELECT ML.ID, Ml.detail1, Ml.detail2
FROM MY_LIST ML
WHERE
ML.ID in (&My_IDS)
Now I can pass in 14001,14002,14003 as a macro.
myListQuery.Active := False;
myListQuery.MacroByName('My_IDS').value := '14001,14002,14003';
myListQuery.Active := True;
i have an issue where, when i go to add more "names" to the if statements. Its hard for me to see if there all ready on there. Thus is there a more clean way to write this Where i can easly see what names are all read there?
function TfDB.GetW(name: string) :integer;
begin
result := 0;
if (name = 'Destacker') or (name='Router') or (name = 'Turn Table') Then
result := 57;
if (name = 'Laser Marker') then
result := 66;
if (name = 'SP28')OR(name='Chamber') OR (name = 'CM402') OR (name = 'SP60') then
result := 65;
if (name = 'Conveyor') OR (name = 'Slide Gate') OR (name = 'Washer') then
result := 51;
if (name = 'BTU') OR (name = 'Furukawa') OR (name = 'Radial') OR (name = 'HDP') or (name = 'MSR') OR (name = 'Koki') Or (name = 'MSF') then
result := 98;
if (name = 'Buffer')OR (name = 'Reclaimer') OR (name = 'ECO Roller') then
result := 49;
if (name = 'Inverter') or (name = 'CNC') then
result := 42;
if (name = '3-D Check Station') or (name='Screw Machine') or (name='VT-Win') or(name='Component Viewer') then
result := 58;
if (name = 'AOI Panel') or (name='AirBlow') then
result := 42;
if (name='Mag Loader') or (name='Soltec') then
result := 73;
if (name='Tester') then
result := 33;
if (name='LoadBox') then
result := 17;
if (name = 'DeltaWave') then
result := 89;
if (name = 'ScrewFeeder') then
result := 25;
if (name='Pump') then
result := 33;
//if result is 0 show message error.
end;
You could create a dictionary, TDictionary<string, Integer>, and store it in a global variable. Load it up with the name to width mapping at initialization time. And then your function becomes a one-liner.
var
WidthMapping: TDictionary<string, Integer>;
....
function TfDB.GetW(name: string) :integer;
begin
if not WidthMapping.TryGetValue(name, Result) then
... handle error condition
end;
....
initialization
WidthMapping := TDictionary<string, Integer>.Create;
WidthMapping.Add('Destacker', 57);
WidthMapping.Add('Router', 57);
... etc.
....
finalization
WidthMapping.Free;
Yes, don't use an if statement but an array and a loop:
const
NAME_RESULT: array [1..2] of record
Name: string;
Value: Integer;
end = (
(Name: 'whatever'; Value: 57)
, (Name: 'Something else'; Value: 57)
);
var
i: Integer;
begin
Result := 0; // or whatever you need your default to be
for i := Low(NAME_RESULT) to High(NAME_RESULT) do
if SameText(name, NAME_RESULT[i].Name) then
begin
Result := NAME_RESULT[i].Value;
Break;
end;
end;
Additional advantage: you don't need to keep the names which return the same values together but can sort the list alphabetically.
Create an array (or dynamic array if you want to further add more names without any concerns of the array dimensions) of strings that contains all the names you want to test for (here I assumed fixed size string array):
var test = array [1..50] of string;
a[1]:='Destacker';
a[2]:='Router';
etc.
In your test routine you may use the case keyword like this:
function TfDB.GetW(index: integer) :integer
begin
result:=0;
case index of
1,2,3: result:=57;
4: result:=66
end
end;
I think it's easier this way
I'm using Delphi XE2 and a TSQLQuery object. It works the first time that I use it. If I immediately reuse it, then it doesn't parse the new SQL for its parameters and rebuild the qry.Params list:
var
qry: TSQLQuery;
begin
qry := TSQLQuery.Create(nil);
try
qry.MaxBlobSize := -1;
qry.SQLConnectin := AnExistingConnection;
qry.CommandText := 'select field1 from table1 where fieldX = #valueX';
qry.ParamByName('valueX').Value := 1;
qry.Open;
// ... use data ...
qry.Close;
qry.Params.Clear; // <- works the same with or without this
qry.CommandText := 'select field2 from table2 where fieldY = #valueY';
qry.ParamByName('valueY').Value := 2; // <- Error: 'valueY' Param not found
qry.Open;
finally
FreeAndNil(qry);
end;
end;
It doesn't matter what I do, it doesn't parse the 2nd SQL statement for its parameters so I can't bind the 'valueY' parameter by name.
I can think of two workarounds:
Manually build the qry.Params list myself.
Destroy and recreate the qry object in between the two commands.
I shouldn't have to do either of these. Perhaps there is a property or something on the qry object that will cause it to reparse parameters each time a new SQL statement is assigned to its CommandText property?
Turned out to be a syntax issue. Params must be prefaced with a : not a #. I had local SQL variables throughout the real first query, so there was a mixture of #param and :param variables throughout the SQL. By using the :param syntax for all bound parameters, the TSQLQuery does properly parse the parameters each time, like it is supposed to do.
var
qry: TSQLQuery;
begin
qry := TSQLQuery.Create(nil);
try
qry.MaxBlobSize := -1;
qry.SQLConnectin := AnExistingConnection;
qry.CommandText := 'select field1 from table1 where fieldX = :valueX';
qry.ParamByName('valueX').Value := 1;
qry.Open;
// ... use data ...
qry.Close;
qry.CommandText := 'select field2 from table2 where fieldY = :valueY';
qry.ParamByName('valueY').Value := 2;
qry.Open;
finally
FreeAndNil(qry);
end;
end;
Use the TSQLQuery.SQL property instead of the TSQLQuery.CommandText property:
qry.SQL.Text := 'select field1 from table1 where fieldX = #valueX';
...
qry.SQL.Text := 'select field2 from table2 where fieldY = #valueY';
No need to call Params.Clear in between, the SQL property will handle that for you.
i got this sp:
DROP TABLE IF EXISTS SplitValuesDump;
CREATE TABLE SplitValuesDump (
value VARCHAR(1000) NOT NULL PRIMARY KEY
);
DELIMITER $$
DROP PROCEDURE IF EXISTS `ChangeSitesRedirects`$$
CREATE PROCEDURE `ChangeSitesRedirects`(
prodimainAddress varchar(255),
subdomainMainAddress varchar(255)
)
SQL SECURITY INVOKER
BEGIN
DECLARE tdomain varchar(1000);
DECLARE tvalue varchar(1000);
DECLARE prepValue varchar(1000);
DECLARE subdomainFullAddress varchar(1000);
DECLARE totalDomain int;
DECLARE tclientid int;
DECLARE sitedone INT DEFAULT 0;
DECLARE splitdone INT DEFAULT 0;
DECLARE lastDomain varchar(1000);
DECLARE curlSites CURSOR FOR (SELECT domain,clientid from sites where redirectsubdomain = 'N');
DECLARE CONTINUE HANDLER FOR NOT FOUND SET sitedone = 1;
set sitedone := 0;
OPEN curlSites;
Scan_Sites:WHILE (sitedone = 0) DO
IF sitedone = 1 THEN
BEGIN
LEAVE Scan_Sites;
END;
ELSE
BEGIN
DECLARE curlStringDump CURSOR FOR (SELECT `value` from SplitValuesDump);
DECLARE CONTINUE HANDLER FOR NOT FOUND SET splitdone = 1;
FETCH curlSites INTO tdomain,tclientid;
CALL split_string(tdomain,';');
OPEN curlStringDump;
SET splitdone:=0;
ScanDump: WHILE (splitdone = 0) DO
IF splitdone = 1 THEN
BEGIN
LEAVE ScanDump;
END;
ELSE
BEGIN
FETCH curlStringDump INTO tvalue;
SET subdomainFullAddress:= subdomainMainAddress;
IF tvalue <> "" THEN
BEGIN
IF tvalue like prodimainAddress OR tvalue like subdomainMainAddress THEN
BEGIN
set totalDomain := totalDomain + 1;
IF tvalue like subdomainMainAddress THEN
BEGIN
SET subdomainFullAddress := tvalue;
END;
END IF;
END;
ELSE
BEGIN
set totalDomain := totalDomain + 1;
set lastDomain := tvalue;
END;
END IF;
END;
END IF;
END;
END IF;
END WHILE ScanDump;
CLOSE curlStringDump;
SET splitdone :=0;
SET prepValue:='N';
IF lastDomain = '' AND totalDomain = 2 THEN
BEGIN
set prepValue := subdomainFullAddress || CHAR(2) || prodimainAddress;
INSERT INTO sites_tmp SELECT * FROM sites where clientid = tclientid limit 1;
UPDATE sites_tmp SET redirectsubdomain = prepValue WHERE clientid = tclientid limit 1;
END;
ELSE
BEGIN
set prepValue := prodimainAddress || CHAR(2) || lastDomain || CHAR(1) ||subdomainFullAddress || CHAR(2) || lastDomain;
INSERT INTO sites_tmp SELECT * FROM sites where clientid = tclientid limit 1;
UPDATE sites_tmp SET redirectsubdomain = prepValue WHERE clientid = tclientid limit 1;
END;
END IF;
END;
END IF;
END WHILE Scan_Sites;
CLOSE curlSites;
SET sitedone :=0;
END$$
i try in the get few info from column data split his data and bring some data ion there.
for each recored on table sites
and then update table sites_tmp.
i got issue that i not know how i can debug at or make at faster?
what ur recommend here?
as well why its so slow???
and in the end its not passed the all the records?