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 the following code but this does not return the resulting rows.
When I don't use dynamic sql, I was able to return the result with the help of the cursor, but not it doesn't return anything.
CREATE OR REPLACE PROCEDURE my_db.sp_test
(
my_clause in VARCHAR2
)
AS
query1 VARCHAR2(5000) DEFAULT 'SELECT my_table.* FROM my_table WHERE 1=1 ';
BEGIN
if like_clause is not null then
query1 := query1 || my_clause;
else
query1 := query1 || my_clause;
end if;
EXECUTE IMMEDIATE query1;
END;
/
for example: my_clause parameter is 'AND my_table.ID IN (10693192,10687172,10630960)'
EDIT:
The following code returns the resulting rows. And I am able to read them from my .NET application (and also from Toad). However, the above query returns nothing.
And I will need t ouse dynamic sql so, I would like to return the same result using the above SP.
CREATE OR REPLACE PROCEDURE PHX_EPI.sp_test2
(
cursor_ OUT SYS_REFCURSOR
)
AS
BEGIN
OPEN cursor_ FOR
SELECT my_table.* FROM my_table;
END;
/
I am using ADOQuery in Delphi 7 and Oracle. I am getting error while passing parameters to ADOQuery. I have used following line. Please help me to identify error.
ADOQuery.Sql.text:= 'select * from temp_table '+
'where column1 in (select column from table2 where id=:id) and id=:id';
ADOQuery.Parameters.ParamByValue('id').value= 'abc';
ADOQuery.open;
when I open the query i will get following error:
Parameter object is improperly defined. Inconsistent or incomplete information is provided.
We have the same problem, we ended "masking" the class TParameters like this:
Declaration:
TMyParameter = class(TParameter)
private
function GetAsValue: variant;
Procedure SetAsValue(const Value: variant);
public
property Value: variant read GetAsValue write SetAsValue;
end;
Implementation:
procedure TMyParameter.SetAsValue(const Value: variant);
var
iPar: Integer;
begin
for iPar:= 0 to Collection.Count - 1 do
if (Name = TParameter(Collection.Items[iPar]).Name) then
TParameter(Collection.Items[iPar]).Value:= Value;
end;
function TMyParameter.GetAsValue: variant;
begin
Result:= inherited Value;
end;
And how to use:
TMyParameter(ADOQuery.Parameters.ParamByName('id')).AsValue:= 'abc';
I hope it helps.
for i:=0 to ADOQuery.Parameters.Count-1 do
begin
if ADOQuery.Parameters.Items[i].Name = 'id' then
ADOQuery.Parameters.Items[i].Value := 'abc';
end;
You need to distinguish between the two id;s:
ADOQuery.Sql.text:= 'select * from temp_table a where column1 in (select column from table2 b where b.id=:id) and a.id=:id';
ADOQuery.Parameters.ParamByValue('id').value= 'abc';
ADOQuery.open;
In the SQL code declare a variable of the necessary type, assign to that variable the parameter; you will be able to use that variable as many times as necessary:
ADOQuery.Sql.text:= 'declare #param varchar(50); set #param = :id; '+
'select * from temp_table '+
'where column1 in (select column from table2 where id=#param) and id=#param';
ADOQuery.Parameters.ParamByValue('id').value= 'abc';
ADOQuery.open;
Regards
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?