I'm designing a function that is part of a larger package. The function is intended to take a District Code and return a collection of unique IDs for 10-15 stores that are assigned to that district. The function is intended to return a collection that can be queried like a table, i.e., using the TABLE function in a SQL statement.
I've created the following Types:
Schema Level type:
create or replace TYPE HDT_CORE_ORGIDS AS TABLE OF CHAR(20);
and a Type inside the Package
TYPE CORE_ORGIDS IS TABLE OF CHAR(20) INDEX BY BINARY_INTEGER;
Here's the function code:
FUNCTION FindDistrictOrgs(
ParamOrgCode VARCHAR2
)
RETURN HDT_CORE_ORGIDS
AS
ReturnOrgs HDT_CORE_ORGIDS := HDT_CORE_ORGIDS();
FDOTemp HDT_CORE_MAIN.CORE_ORGIDS;
i BINARY_INTEGER := 0;
CURSOR FDOCurr IS
SELECT org.id AS OrgID
FROM tp2.tpt_company org
WHERE LEVEL = 2
START WITH org.name = ParamOrgCode
CONNECT BY PRIOR org.id = org.parent_id;
BEGIN
OPEN FDOCurr;
LOOP
i := i +1;
FETCH FDOCurr INTO FDOTemp(i);
EXIT WHEN FDOCurr%NOTFOUND;
END LOOP;
IF FDOTemp.EXISTS(FDOTemp.FIRST) THEN
ReturnOrgs.EXTEND(FDOTemp.LAST);
FOR x IN FDOTemp.FIRST .. FDOTemp.LAST LOOP
ReturnOrgs(x) := FDOTemp(x).OrgID;
END LOOP;
END IF;
CLOSE FDOCurr;
RETURN ReturnOrgs;
END FindDistrictOrgs ;
I'm getting the PLS-00487:Invalid Reference to variable 'CHAR' at the line:
ReturnOrgs(x) := FDOTemp(x).OrgID;
I've double-checked at the value returned by the SQL (the org.id AS OrgID) is of the CHAR(20 BYTE) datatype.
So...what's causing the error?
Any help is appreciated! :)
OrgID is the alias you gave the column in your cursor, it has no meaning to the collection. Since both collections are of simple types you should just be doing:
ReturnOrgs(x) := FDOTemp(x);
The syntax you're using is implying FDOTemp is a collection of objects and you're trying to reference the OrgID attribute of an object; but since CHAR isn't an object type, this errors. The error message even makes some sense when viewed like that, though it's not terribly helpful if you don't already know what's wrong... and not entirely helpful when you do.
Incidentally, you could use a bulk collect to populate the collection without the cursor or loops, or the extra collection:
SELECT org.id
BULK COLLECT INTO ReturnOrgs
FROM tp2.tpt_company org
WHERE LEVEL = 2
START WITH org.name = ParamOrgCode
CONNECT BY PRIOR org.id = org.parent_id;
RETURN ReturnOrgs;
Related
I want to leave some fields empty (i.e. Null) when I insert values into table. I don't see why would I want to have a DB full of empty strings in fields.
I use Delphi 10, FireDAC and local SQLite DB.
Edit: Provided code is just an example. In my application values are provided by user input and functions, any many of them are optional. If value is empty, I would like to keep it at Null or default value. Creating multiple variants of ExecSQL and nesting If statements isn't an option too - there are too many optional fields (18, to be exact).
Test table:
CREATE TABLE "Clients" (
"Name" TEXT,
"Notes" TEXT
);
This is how I tried it:
var someName,someNote: string;
begin
{...}
someName:='Vasya';
someNote:='';
FDConnection1.ExecSQL('INSERT OR REPLACE INTO Clients(Name,Notes) VALUES (:nameval,:notesval)',
[someName, IfThen(someNote.isEmpty, Null, somenote)]);
This raises an exception:
could not convert variant of type (Null) into type (OleStr)
I've tried to overload it and specify [ftString,ftString] and it didn't help.
Currently I have to do it like this and I hate this messy code:
FDConnection1.ExecSQL('INSERT OR REPLACE INTO Clients(Name,Notes) VALUES ('+
IfThen(someName.isEmpty,'NULL','"'+Sanitize(someName)+'"')+','+
IfThen(someNote.isEmpty,'NULL','"'+Sanitize(someNote)+'"')+');');
Any recommendations?
Edit2: Currently I see an option of creating new row with "INSERT OR REPLACE" and then use multiple UPDATEs in a row for each non-empty value. But this looks direly ineffective. Like this:
FDConnection1.ExecSQL('INSERT OR REPLACE INTO Clients(Name) VALUES (:nameval)',[SomeName]);
id := FDConnection1.ExecSQLScalar('SELECT FROM Clients VALUES id WHERE Name=:nameval',[SomeName]);
if not SomeString.isEmpty then
FDConnection1.ExecSQL('UPDATE Clients SET Notes=:noteval WHERE id=:idval)',[SomeNote,id]);
According to Embarcadero documentation ( here ):
To set the parameter value to Null, specify the parameter data type,
then call the Clear method:
with FDQuery1.ParamByName('name') do begin
DataType := ftString;
Clear;
end;
FDQuery1.ExecSQL;
So, you have to use FDQuery to insert Null values, I suppose. Something like this:
//Assign FDConnection1 to FDQuery1's Connection property
FDQuery1.SQL.Text := 'INSERT OR REPLACE INTO Clients(Name,Notes) VALUES (:nameval,:notesval)';
with FDQuery1.ParamByName('nameval') do
begin
DataType := ftString;
Value := someName;
end;
with FDQuery1.ParamByName('notesval') do
begin
DataType := ftString;
if someNote.IsEmpty then
Clear;
else
Value := someNote;
end;
if not FDConnection1.Connected then
FDConnection.Open;
FDQuery1.ExecSql;
It's not very good idea to execute query as String without parameters because this code is vulnerable to SQL injections.
Some sources tells that it's not enough and you should do something like this:
with FDQuery1.ParamByName('name') do begin
DataType := ftString;
AsString := '';
Clear;
end;
FDQuery1.ExecSQL;
but I can't confirm it. You can try it if main example won't work.
I am working on Oracle 11g Db, Having trouble on writing Oracle syntax.
I am trying to pass a number variable to my select query and populate the select query to a cursor.
Declare yr_nr NUMBER;
Begin
yr_nr := 2014;
SELECT DCD.CCY ID, DCD.CCYCDDSC DSC
FROM CCYDCD DCD, CCYEXC EXC
WHERE DCD.CCY = EXC.CCY
AND EXC.YEARNR = yr_nr
End
This select query returns 80 records. How to rewrite this syntax.
Ok, so what you have here is an anonymous block and everything that happens in the block stays in that block. Kinda like Vegas.
In other words there is nothing to handle the result set from your query. When you do this:
declare
[varName] [type]
begin
select foo from bar where column = var ; <--- this has no place to go!
end
When you are at an sqlPlus prompt, sqlPlus has a default record set handler which then processes the returned record set and prints it to the screen.
When you use any third party tool like JDBC or Oracle's own OCI library those provide a record set handler then parse them to you with the appropriate calls to get the data, e.g.:
rs.getInteger([query],[column] ) //which returns the specific value.
That anonymous block is essentially a stored procedure. So you have to have something to do with the result set. This is the cause of the missing "into" error you are getting.
If on the other hand you did something like:
declare
[varName] [type]
result number ;
begin
select count(foo) into result from bar where column = var ;
end
The variable result would have the value of 80 since that is the number of records fetched.
declare
[varName] [type]
cursor thisCursor(p1 in number ) is select foo from bar where column = p1 ;
begin
for rec in thisCursor(varName) loop
If rec.column = [some value] then
doSomething
end if ;
end loop ;
end
Do this would allow you to do something with the result set.
I am trying to get a ref_cursor to be assigned to a variable inside a for loop then returned at the end of a function. The loop in going through a local cursor if it gets more than 1 result.
I have noted where the error occurs in the code. I am not sure how to create a loop where i can get a ref_cursor for the current point in the loop, assign it to a variable and then return it to the function. Could someone someone help me figure out how to do that? Below is my i-th attempt at logic based of reading around the google searches.
The error is "PLS-00382: expression is of wrong type" and i know that i obviously am not assigning the correct variably type based on this error but the code below with the error is an illustration of what I want to do and what I need help accomplishing.
FUNCTION GET_PARCEL(p_lat in number, p_long in number) return sys_refcursor
IS
v_distance number(10) := 100000000;
v_shortest_dist number(10) := v_distance;
v_centroid SDO_GEOMETRY;
v_rc_ref_cursor sys_refcursor;
v_ref_geom SDO_GEOMETRY := mdsys.SDO_GEOMETRY(2001, 8311, NULL, SDO_ELEM_INFO_ARRAY(1, 1, 1), SDO_ORDINATE_ARRAY(120.3214, -10.7088));
cursor query_cursor is select * from PARCEL_TABLE where code = 20134;
BEGIN
for query_row in query_cursor loop
v_centroid := SDO_GEOM.SDO_CENTROID(query_row.geometry, 0.05);
IF (v_centroid is not null) then
v_distance := SDO_GEOM.SDO_DISTANCE(v_centroid, v_ref_geom, 0.05);
IF v_distance < v_shortest_dist THEN
v_shortest_dist := v_distance;
v_rc_ref_cursor := query_row; -- Error on this line
END IF;
ELSE
DBMS_OUTPUT.PUT_LINE('Centroid is not initialised for some reason.');
END IF;
end loop;
return v_rc_ref_cursor;
END;
As far as I know, you cannot build up a cursor. Cursors are created and maintained by the database with connections to their source data, transaction and session context, and the like.
I would suggest you declare a type, instantiate it, build up the values in the type.
When you are done, create a cursor by selecting * from table (cast (variable as your_type)).
Munge the cursor into a ref_cursor and return that.
Pro tip is to remember you have data in your table and you can use it for logic. Turns out if I use the IDENTIFIER or ROWID of the row/s which i want then i can do a where clause into a ref cursor that looks up by IDENTIFIER or ROWID.
eg.
open v_rc_ref_cursor for select * from PARCEL_TABLE n where n.identifier = query_row.identifier;
super simple stuff :)
I am trying to write a following PL/SQL function body for a dynamic action
The purpose of dynamic action is to set value for text area based on input parameters. Way I am trying to do it, is that setting the value into variable for different options
declare
P_NOTE varchar(100); -- derive value
P_WEBSERVER varchar(100); -- derive name
begin
-- for getting the P_NOTE value
select distinct note into P_NOTE from port_mapping where PLATFORM = :P3_PLATFORM and VERSION = :P3_VERSION;
-- for getting web server value
select CONCAT(P_NOTE,CONCAT('https-',:P3_CLIENT)) into P_WEBSERVER from dual order by 1;
if (:P3_PLATFORM = 'Apache') then
return P_WEBSERVER;
end if;
end;
However I am getting error
ORA-06550: line 15, column 5:
PLS-00372: In a procedure, RETURN statement cannot contain an expression
ORA-06550: line 15, column 5:
PL/SQL: Statement ignored
declare
P_NOTE varchar(100);
P_WEBSERVER varchar(100);
I am not sure what I am missing.
(Since you did not post any apex version this explanation deals with version 4.2)
If this -is- a dynamic action and the code you posted is in a true action of type 'Execute PL/SQL Code' then you can not use RETURN. The plsql block is not a function body (close, Mr Kemp!).
If you want to return values from the session state to page items then you need to use the "Page Items to Return" item of the true action.
This will put the session state of the defined page items into the value of the item on the page. This means that you can not use any variable to just put stuff in to be able to return it to the page, but you need to use an actual page item (after all, these are bind variables).
To clarify further, you would not write :
return P_WEBSERVER;
But you'd have to use a page item, say P3_WEBSERVER, and you'll need to create one if it doesn't exist of course:
:P3_WEBSERVER := p_webserver;
Of course you'd need to make sure that the correct value will be in there as you can not shortcircuit as you did in your code sample (p_webserver will usually hold a value even if the platform is not 'Apache') eg:
if (:P3_PLATFORM = 'Apache') then
:P3_WEBSERVER := P_WEBSERVER;
else
:P3_WEBSERVER := NULL;
end if;
Just read error message:
line 15, column 5
So, trouble caused by this line:
return P_WEBSERVER;
return not allowed in PL/SQL blocks, use output parameter to return a value.
Read Tom's answer to find out how to do that.
What is the diffecence between adding INDEX BY PLS_INTEGER and not at end of declaration of new table type. Look at this example:
DECLARE
GC_BULK_LIMIT CONSTANT INTEGER := 500;
CURSOR CUR_CLIENTS IS SELECT C.ID, C.NAME FROM CLIENTS C;
TYPE RT_CLIENTS IS TABLE OF CUR_CLIENTS%ROWTYPE;
-- TYPE RT_CLIENTS IS TABLE OF CUR_CLIENTS%ROWTYPE INDEX BY PLS_INTEGER;
LT_CLIENTS RT_CLIENTS;
BEGIN
OPEN CUR_CLIENTS;
LOOP
FETCH CUR_CLIENTS BULK COLLECT INTO LT_CLIENTS LIMIT GC_BULK_LIMIT;
EXIT WHEN LT_CLIENTS.COUNT = 0;
FOR I IN 1..LT_CLIENTS.COUNT LOOP
-- ... SOME LOGIC
END LOOP;
END LOOP;
CLOSE CUR_CLIENTS;
END;
in response to "must i add". The short answer is NO.
the difference is that
TYPE RT_CLIENTS IS TABLE OF CUR_CLIENTS%ROWTYPE;
Is a nested table. This means that for a given variable of this type, we know that the subscripts are sequential. i.e. the subscript starts from 1 and goes up to the array length.
The following loop therefore, is the right way to access a nested table array:
FOR I IN 1..LT_CLIENTS.COUNT LOOP
This however, is called a associative array:
TYPE RT_CLIENTS IS TABLE OF CUR_CLIENTS%ROWTYPE INDEX BY PLS_INTEGER;
(you could also index by a varchar2 if you wanted). The difference is that the subscripts in this case do not have to be sequential, depending on how the array was populated. In your code, they would be (as bulk collect would do that), but its not always the case.
The safe way to access and loop through an index by array is :
v_subscript := t_arr.first;
while v_subscript is not null loop
dbms_output.put_line(v_subscript || ': ' || t_arr(v_subscript));
v_subscript := t_arr.next(v_subscript);
end loop;
where v_subscript is a variable of the same datatype of the index by part.
also with a nested table, you can populate the array quickly with:
declare
type myarr is table of number;
t_arr myarr;
v_subscript number;
begin
t_arr := myarr(1, 12, 44);
whereas with an index by array you'd have to have three lines there to populate it:
t_arr(1):= 1;
t_arr(2):= 12;
t_arr(3):= 44;
for your particular case, without the index by is perfectly fine.
further reading: http://docs.oracle.com/cd/E18283_01/appdev.112/e17126/composites.htm