PL/SQL 11g > Getting Data into a Table of Records Object - oracle

I'm trying to pull data from a complex query (it's been simplified here for review) using custom RECORD and TABLE OF RECORD data types, but I can't get data into the table due to a "PLS-00308: This construct is not allowed as the origin of an assignment" Error. I've followed the examples carefully and don't understand the problem. Can anyone point me in a direction.
here's the code
TYPE CORE_REC IS RECORD
(
OrgID CHAR(20 BYTE)
, StoreNumber VARCHAR2(200 BYTE)
, StoreName VARCHAR(200 BYTE)
, AssociateName VARCHAR2(300 BYTE)
);
TYPE CORE_REC_CURSOR IS REF CURSOR RETURN CORE_REC;
TYPE CORE_REC_TABLE IS TABLE OF CORE_REC INDEX BY BINARY_INTEGER;
FUNCTION CORE_GETCURRS (
OrgID IN CHAR
) RETURN HDT_CORE_MAIN.CORE_REC AS
CurrTable HDT_CORE_MAIN.CORE_REC;
i BINARY_INTEGER := 0;
CURSOR CurrCursor IS
WITH
CoreCurrs AS
(SELECT
busSTR.id AS OrgID
, busSTR.name AS StoreNumber
, busSTR.name2 AS StoreName
, emp.lname || ', ' || emp.fname || ' ' || emp.mname AS AssociateName
FROM tp2.tpt_company busSTR
INNER JOIN tp2.cmt_person emp
ON busSTR.ID = emp.company_id
WHERE
busSTR.id = OrgID
)
SELECT
CoreCurrs.OrgID
, CoreCurrs.StoreNumber
, CoreCurrs.StoreName
, CoreCurrs.AssociateName
FROM CoreCurrs
;
BEGIN
DBMS_OUTPUT.ENABLE(1000000);
OPEN CurrCursor;
LOOP
i := i + 1;
FETCH CurrCursor INTO CurrTable(i);
EXIT WHEN CurrCursor%NOTFOUND;
END LOOP;
CLOSE CurrCursor;
RETURN CurrTable;
END CORE_GETCURRS;
The error gets thrown at the FETCH statement.

Your variable is the wrong type, it should be:
CurrTable HDT_CORE_MAIN.CORE_REC_TABLE;
At the moment you're trying to select into an element of a record, rather than element of a table, which doesn't make sense. When it's defined as CORE_REC, referring to CurrTable(i) doesn't mean anything.

Related

Oracle query result as JSON

I'm working in Oracle 12.2.
I've got a complex query the results of which I would like to receive as a CLOB in JSON format. I've looked into json_object, but this means completely rewriting the query.
Is there a way to simply pass the ref cursor or result set and receive a JSON array with each row being a JSON object inside?
My query:
SELECT
*
FROM
(
SELECT
LABEL_USERS.*,
ROWNUM AS RANK ,
14 AS TOTAL
FROM
(
SELECT DISTINCT
SEC_VS_USER_T.USR_ID,
SEC_VS_USER_T.USR_FIRST_NAME,
SEC_VS_USER_T.USR_LAST_NAME,
SEC_USER_ROLE_PRIV_T.ROLE_ID,
SEC_ROLE_DEF_INFO_T.ROLE_NAME,
1 AS IS_LABEL_MANAGER,
LOWER(SEC_VS_USER_T.USR_FIRST_NAME ||' '||SEC_VS_USER_T.USR_LAST_NAME) AS
SEARCH_STRING
FROM
SEC_VS_USER_T,
SEC_USER_ROLE_PRIV_T,
SEC_ROLE_DEF_INFO_T
WHERE
SEC_VS_USER_T.USR_ID = SEC_USER_ROLE_PRIV_T.USR_ID
AND SEC_VS_USER_T.USR_SITE_GRP_ID IS NULL
ORDER BY
UPPER(USR_FIRST_NAME),
UPPER(USR_LAST_NAME)) LABEL_USERS) LABEL_USER_LIST
WHERE
LABEL_USER_LIST.RANK >= 0
AND LABEL_USER_LIST.RANK < 30
I couldn't find a procedure which I could use to generate the JSON, but I was able to use the new 12.2 functions to create the JSON I needed.
SELECT JSON_ARRAYAGG( --Used to aggregate all rows into single scalar value
JSON_OBJECT( --Creating an object for each row
'USR_ID' VALUE USR_ID,
'USR_FIRST_NAME' VALUE USR_FIRST_NAME,
'USR_LAST_NAME' VALUE USR_LAST_NAME,
'IS_LABEL_MANAGER' VALUE IS_LABEL_MANAGER,
'SEARCH_STRING' VALUE SEARCH_STRING,
'USR_ROLES' VALUE USR_ROLES
)returning CLOB) AS JSON --Need to cpecify CLOB, otherwise the result is limited by VARCHARC2(4000)
FROM
(
SELECT * FROM (
SELECT LABEL_USERS.*, ROWNUM AS RANK, 14 AS TOTAL from
(SELECT
SEC_VS_USER_T.USR_ID,
SEC_VS_USER_T.USR_FIRST_NAME,
SEC_VS_USER_T.USR_LAST_NAME,
1 AS IS_LABEL_MANAGER,
LOWER(SEC_VS_USER_T.USR_FIRST_NAME ||' '||SEC_VS_USER_T.USR_LAST_NAME) AS SEARCH_STRING,
(
SELECT --It is much easier to create the JSON here and simply use this column in the outer JSON_OBJECT select
JSON_ARRAYAGG(JSON_OBJECT('ROLE_ID' VALUE ROLE_ID,
'ROLE_NAME' VALUE ROLE_NAME)) AS USR_ROLES
FROM
(
SELECT DISTINCT
prv.ROLE_ID,
def.ROLE_NAME
FROM
SEC_user_ROLE_PRIV_T prv
JOIN
SEC_ROLE_DEF_INFO_T def
ON
prv.ROLE_ID = def.ROLE_ID
ORDER BY
ROLE_ID DESC)) AS USR_ROLES
FROM
SEC_VS_USER_T,
SEC_USER_ROLE_PRIV_T,
SEC_ROLE_DEF_INFO_T
WHERE
SEC_VS_USER_T.USR_ID = SEC_USER_ROLE_PRIV_T.USR_ID
AND SEC_USER_ROLE_PRIV_T.ROLE_PRIV_ID = SEC_ROLE_DEF_INFO_T.ROLE_ID
AND SEC_VS_USER_T.USR_SITE_GRP_ID IS NULL
ORDER BY UPPER(USR_FIRST_NAME),
UPPER(USR_LAST_NAME))LABEL_USERS)) LABEL_USER_LIST
WHERE LABEL_USER_LIST.RANK >= 0--:bv_Min_Rows
AND LABEL_USER_LIST.RANK < 30--:bv_Max_Rows

PL/SQL Trigger Variable Problems

I am relatively new to PL/SQL and i am trying to create a trigger that will alert me after an UPDATE on a table Review. When it is updated I want to ge the username(User table), score(Review Table), and product name (Product Table) and print them out:
This is what I have so far:
three tables:
Review: score, userid,pid, rid
Users: userid,uname
Product: pid,pname
So Review can reference the other tables with forigen keys.
create or replace trigger userNameTrigger
after insert on review
for each row
declare
x varchar(256);
y varchar(256);
z varchar(256);
begin
select uname into x , pname into y , score into z
from review r , product p , users u
where r.pid = p.pid and r.userid = u.userid and r.rid =new.rid;
dbms_output.put_line('user: '|| X||'entered a new review for Product: '|| Y || 'with a review score of: '|| Z);
end;
The problem I am having is I cannot seem to figure out how to store the selected fields into the variables and output it correctly.
DDL:
Create Table Review
(
score varchar2(100)
, userid varchar2(100)
, pid varchar2(100)
, rid varchar2(100)
);
Create Table Users
(
userid varchar2(100)
, uname varchar2(100)
);
Create Table Product
(
pid varchar2(100)
, pname varchar2(100)
);
The first problem I can see is that you're missing a colon when you refer to new.rid. The second is that you're accessing the review table inside a row-level trigger on that same table, which will give you a mutating table error at some point; but you don't need to as all the data from the inserted row is in the new pseudorow.
create or replace trigger userNameTrigger
after insert on review
for each row
declare
l_uname users.uname%type;
l_pname product.pname%type;
begin
select u.uname into l_uname
from users u
where u.userid = :new.userid;
select p.pname
into l_pname
from product
where p.pid = :new.pid;
dbms_output.put_line('user '|| l_uname
|| ' entered a new review for product ' || l_pname
|| ' with a review score of '|| :new.score);
end;
The bigger problem is that the only person who could see the message is the user inserting tow row, which seems a bit pointless; and they would have to have output enabled in their session to see it.
If you're trying to log that so someone else can see it then store it in a table or write it to a file. As the review table can be queried anyway it seems a bit redundant though.
Having all your table columns as strings is also not good - don't store numeric values (e.g. scores, and probably the ID fields) or dates as strings, use the correct data types. It will save you a lot of pain later. You also don't seem to have any referential integrity (primary/foreign key) constraints - so you can review a product that doesn't exist, for instance, which will cause a no-data-found exception in the trigger.
It makes really no sense to use a trigger to notify themselves about changed rows. If you insert new rows into the table, then you have all info about them. Why not something like the block below instead a trigger:
create table reviews as select 0 as rid, 0 as userid, 0 as score, 0 as pid from dual where 1=0;
create table users as select 101 as userid, cast('nobody' as varchar2(100)) as uname from dual;
create table products as select 1001 as pid, cast('prod 1001' as varchar2(100)) as pname from dual;
<<my>>declare newreview reviews%rowtype; uname users.uname%type; pname products.pname%type; begin
insert into reviews values(1,101,10,1001) returning rid,userid,score,pid into newreview;
select uname, pname into my.uname, my.pname
from users u natural join products p
where u.userid = newreview.userid and p.pid = newreview.pid
;
dbms_output.put_line('user: '||my.uname||' entered a new review for Product: '||my.pname||' with a review score of: '||newreview.score);
end;
/
output: user: nobody entered a new review for Product: prod 1001 with a review score of: 10
In order to inform another session about an event you should use dbms_alert (transactional) or dbms_pipe (non transactional) packages. An example of dbms_alert:
create or replace trigger new_review_trig after insert on reviews for each row
begin
dbms_alert.signal('new_review_alert', 'signal on last rid='||:new.rid);
end;
/
Run the following block in another session (new window, worksheet, sqlplus or whatever else). It will be blocked until the registered signal is arrived:
<<observer>>declare message varchar2(400); status integer; uname users.uname%type; pname products.pname%type; score reviews.score%type;
begin
dbms_alert.register('new_review_alert');
dbms_alert.waitone('new_review_alert', observer.message, observer.status);
if status != 0 then raise_application_error(-20001, 'observer: wait on new_review_alert error'); end if;
select uname, pname, score into observer.uname, observer.pname, observer.score
from reviews join users using(userid) join products using (pid)
where rid = regexp_substr(observer.message, '\w+\s?rid=(\d+)', 1,1,null,1)
;
dbms_output.put_line('observer: new_review_alert for user='||observer.uname||',product='||observer.pname||': score='||observer.score);
end;
/
Now in your session:
insert into reviews values(2, 101,7,1001);
commit; --no alerting before commit
The another (observer) session will be finished with the output:
observer: new_review_alert for user=nobody,product=prod 1001: score=7
P.S. There was no RID in the Table REVIEW, so i'll just assume it was supposed to be PID.
create or replace trigger userNameTrigger
after insert on review
for each row
declare
x varchar2(256);
y varchar2(256);
z varchar2(256);
BEGIN
select uname
, pname
, score
INTO x
, y
, z
from review r
, product p
, users u
where r.pid = p.pid
and r.userid = u.userid
and r.PID = :new.pid;
dbms_output.put_line('user: '|| X ||'entered a new review for Product: '|| Y || 'with a review score of: '|| Z);
end userNameTrigger;
You just made a mistake on the INTO statement, you can just clump them together in one INTO.

PL/SQL how to return a User-Defined Record from create or replace function

I'm trying to learn PL/SQL
and I do not seem to understand how I can create a function and let it return a Record
I am trying to do something like this:
create or replace FUNCTION getMovie(movieID number)
RETURN record IS titleAndYear record(title varchar(100), production_Year number);
BEGIN
if (titleAndYear is null) then
titleAndYear:= MovieTitleAndYear('',0);
end if;
select TITLE ,YEAR into titleAndYear.title ,titleAndYear.production_Year from movie where MOVIE_ID = movieID;
return titleAndYear;
END;
I know this is not working but I do not know why ?
EDIT 1:
I have also Tried this:
create or replace TYPE MovieTitleAndYear is OBJECT(title varchar(100), production_Year number);
/
create or replace FUNCTION getMovie(movieID number)
RETURN MovieTitleAndYear IS titleAndYear MovieTitleAndYear;
BEGIN
if (titleAndYear is null) then
titleAndYear:= MovieTitleAndYear('',0);
end if;
select TITLE ,YEAR into titleAndYear.title ,titleAndYear.production_Year from movie where MOVIE_ID = movieID;
return titleAndYear;
END;
But then the result when i run this statement:
select
GETMOVIE(
2540943
) from dual;
becomes this
GETMOVIE(2540943)
1 [DB_036.MOVIETITLEANDYEAR]
instead of two colums title and productionyear.
Your first example using a record won't work. For a start, a function cannot return a value of a type that is only declared inside the function. You can try moving the record type declaration to a package, but even if you then got the function to compile, running the query select getMovie(2540943) from dual would return an ORA-00902 invalid datatype error.
So I would recommend that you use a type instead.
If you are using a type, then to get the movie and year separately, you need to access the fields within the type individually, for example:
select getMovie(2540943).title, getMovie(2540943).production_year from dual
Alternatively, you can use a subquery if you want to avoid calling getMovie() twice:
select x.movie_info.title, x.movie_info.production_year from (select getMovie(2540943) as movie_info from dual) x;
Note that we need to use an alias for the subquery. The following will give an ORA-00904: "MOVIE_INFO"."PRODUCTION_YEAR": invalid identifier error:
select movie_info.title, movie_info.production_year from (select getMovie(2540943) as movie_info from dual);
The problem here is that Oracle is looking for a table movie_info in the query, but it can't find one. It doesn't realise that movie_info is actually a column. If we introduce the alias x, Oracle then realises that x.movie_info is a column and so x.movie_info.title is a field within a type in the column.
Try this approach. I think your answer lies within this snippet. Let
me know if this helps.
--Create object type
CREATE OR REPLACE type av_obj_test
IS
object
(
col1 VARCHAR2(100),
col2 VARCHAR2(100) );
--C reate table type
CREATE OR REPLACE type av_ntt_test
IS
TABLE OF av_obj_test;
--Createfunction
CREATE OR REPLACE FUNCTION AV_RECORD RETURN
AV_NTT_TEST
AS
av_record av_ntt_test;
BEGIN
NULL;
SELECT av_obj_test(LEVEL,'av'||LEVEL) BULK COLLECT INTO av_record FROM DUAL
CONNECT BY LEVEL < 10;
RETURN av_record;
END;
--Calling function
SELECT * FROM TABLE(AV_RECORD);
--------------------------------OUTPUT-------------------------------------
COL1 COL2
1 av1
2 av2
3 av3
4 av4
5 av5
6 av6
7 av7
8 av8
9 av9
-------------------------------OUTPUT----------------------------------------

Oracle PL/sql if input varchar param is empty, how to return all the records?

I have below query. If the input parameter is not null and passed a value, it will return all the matching records for that URL.
if the Input parameter is empty, it should return all the records. How to fix that Update condition?
CREATE OR REPLACE PROCEDURE GetDatesList (
p_URL IN varchar2,
P_RECORDSET OUT SYS_REFCURSOR
)
as
begin
OPEN P_RECORDSET FOR
select
PCBS.KeyId as Key, PCBS.PublishDate
from (select
Serlog.*, PS.ServerType, row_number() over (partition by Serlog.KeyId order by Serlog.PublishDate desc) as RowNu
from PConfigByServerLog Serlog
join PServer PS on PS.ServerName = Serlog.ServerName
and Serlog.URL = PS.URL
where Serlog.URL = p_URL
--Here if we pass a value f p_podURL, we get those matching records back. If it is empty, then it should bring all the records back
) PCBS
where PCBS.RowNu = 1 and PCBS.IsActive = 'T';
end;
where Serlog.URL = p_URL OR p_URL is null
or
where Serlog.URL = nvl(p_URL, Serlog.URL)
Note about performance
One of the other answers warned about performance.
If Serlog.URL is indexed, Oracle will be smart enough to use it if p_URL is not null.
Consider a SQL like this:
SELECT * FROM sys.obj$
where obj# = nvl(:b1, obj# )
The plan would be:
7 SELECT STATEMENT
6 CONCATENATION
2 FILTER
1 TABLE ACCESS FULL SYS.OBJ$
5 FILTER
4 TABLE ACCESS BY INDEX ROWID SYS.OBJ$
3 INDEX RANGE SCAN SYS.I_OBJ1
The two filter operations (#2 and #5) are :b1 is null and :b1 is not null, respectively. So, Oracle will only execute whichever branch of the plan make sense, depending on whether the parameter has been given.
Just write
where (Serlog.URL = p_URL) OR p_URL is null
Instead of
where Serlog.URL = p_URL
Keep in mind that this would give you suboptimal ececution plan: if the url parameter is indexed it might not use the index.
If performance is something to be concerned, you'd better handle separately the two cases with two different cursors
You could also set a default value for p_URL in the procedure definition if that is appropriate:
CREATE OR REPLACE PROCEDURE GetDatesList (
p_URL IN varchar2 DEFAULT '//svr1/dir1/folder2/etc',
P_RECORDSET OUT SYS_REFCURSOR
)
as
So if NULL is passed in this value will be used instead.

Strange Oracle XMLType.getClobVal() result

I use Oracle 11g (on Red Hat). I have simple regular table with XMLType column:
CREATE TABLE PROJECTS
(
PROJECT_ID NUMBER(*, 0) NOT NULL,
PROJECT SYS.XMLTYPE,
);
Using Oracle SQL Developer (on Windows) I do:
select T1.PROJECT P1 from PROJECTS T1 where PROJECT_ID = '161';
It works. I get one cell. I can double click and download whole XML file.
Then I tried to get result as CLOB:
select T1.PROJECT.getClobVal() P1 from PROJECTS T1 where PROJECT_ID = '161';
It works. I get one cell. I can double click and see whole text and copy it. BUT there is a problem. When I copy it to clipboard I get only first 4000 characters. It seems that there is 0x00 character at position 4000 and the rest of CLOB is not copied.
To confirm this, I wrote check in java:
// ... create projectsStatement
Reader reader = projectsStatement.getResultSet().getCharacterStream( "P1" );
BufferedReader bf = new BufferedReader( reader );
char buffer[] = new char[ 1024 ];
int count = 0;
int globalPos = 0;
while ( ( count = bf.read( buffer, 0, buffer.length ) ) > 0 )
for ( int i = 0; i < count; i++, globalPos++ )
if ( buffer[ i ] == 0 )
throw new Exception( "ZERO at " + Integer.toString(globalPos) );
Reader returns full XML but my exception is thrown because there is null character at position 4000. I could remove this single byte but this would be rather strange workaround.
I don't use VARCHAR2 there but maybe this problem is related to VARCHAR2 limitation (4000 bytes) somehow ? Any other ideas ? Is this an Oracle bug or am I missing something ?
-------------------- Edit --------------------
Value was inserted using following stored procedure:
create or replace
procedure addProject( projectId number, projectXml clob ) is
sqlstr varchar2(2000);
begin
sqlstr := 'insert into projects ( PROJECT_ID, PROJECT ) VALUES ( :projectId, :projectData )';
execute immediate sqlstr using projectId, XMLTYPE(projectXml);
end;
Java code used to call it:
try ( CallableStatement cs = connection.prepareCall("{call addProject(?,?)}") )
{
cs.setInt( "projectId", projectId );
cs.setCharacterStream( "projectXml", new StringReader(xmlStr) , xmlStr.length() );
cs.execute();
}
-------------------- Edit. SIMPLE TEST --------------------
I will use all I learned from your answers. Create simplest table:
create table T1 ( P XMLTYPE );
Prepare two CLOBs with XMLs. First with null character, second without.
declare
P1 clob;
P2 clob;
P3 clob;
begin
P1 := '<a>';
P2 := '<a>';
FOR i IN 1..1000 LOOP
P1 := P1 || '0123456789' || chr(0);
P2 := P2 || '0123456789';
END LOOP;
P1 := P1 || '</a>';
P2 := P2 || '</a>';
Check if null is in the first CLOB and not in the second one:
DBMS_OUTPUT.put_line( DBMS_LOB.INSTR( P1, chr(0) ) );
DBMS_OUTPUT.put_line( DBMS_LOB.INSTR( P2, chr(0) ) );
We will get as expected:
14
0
Try to insert first CLOB into XMLTYPE. It will not work. It is not possible to insert such value:
insert into T1 ( P ) values ( XMLTYPE( P1 ) );
Try to insert second CLOB into XMLTYPE. It will work:
insert into T1 ( P ) values ( XMLTYPE( P2 ) );
Try to read inserted XML into third CLOB. It will work:
select T.P.getClobVal() into P3 from T1 T where rownum = 1;
Check if there is null. There is NO null:
DBMS_OUTPUT.put_line( DBMS_LOB.INSTR( P3, chr(0) ) );
It seams that there is no null inside database and as long as we are in the PL/SQL context, there is no null. But when I try to use following SQL in SQL Developer ( on Windows ) or in Java ( on Red Hat EE and Tomcat7 ) I get null character at position 4000 in all returned CLOBs:
select T.P.getClobVal() from T1 T;
BR,
JM
It's not an Oracle bug (it stores and retrieves the \0 just fine. It's a client/windows bug (Different clients behave differently in regards to "NUL" as does windows)
chr(0) is not a valid character in non-blobs really (I'm curious how you ever get the XMLType to accept it in the first place as usually it wouldn't parse).
\0 is used in C to denote the end of a string (NUL terminator) and some GUIs would stop processing the string at that point. For example:
![SQL> select 'IM VISIBLE'||chr(0)||'BUT IM INVISIBLE'
2 from dual
3 /
'IMVISIBLE'||CHR(0)||'BUTIM
---------------------------
IM VISIBLE BUT IM INVISIBLE
SQL>
yet toad fails miserably on this:
sql developer fares better, as you can see it:
but if you copy it, the clipboard will only copy it up to the nul character. this copy paste error isn't SQL developers fault though, it's a problem with windows clipboard not allowing NUL to paste properly.
you should just replace(T1.PROJECT.getClobVal(), chr(0), null) to get round this when using sql developer/windows clipboard.
I also was experiencing this same issue exactly as described by Mikosz (seeing an extra 'NUL' character around the 4000th character when outputting my XMLType value as a Clob). While playing around in SQLDeveloper I noticed an interesting workaround. I was trying to see the output of my XMLType, but was tired of scrolling to the 4000th character, so I started wrapping the Clob output in a substr(...). Much to my surprise, the issue actually disappeared. I incorporated this into my Java app and confirmed that the issue was no longer present and my Clob could be retrieved without the extra character. I know that this isn't an ideal workaround, and I'm still not sure why it works (would love if someone could explain it to me), but here's an abbreviated example of what I've currently got working:
// Gets the xml contents
String sql = "select substr(x.xml_content.getClobVal(), 0) as xml_content from my_table x";
ps = con.prepareStatement(sql);
if(rs.next()) {
Reader reader = new BufferedReader(rs.getCharacterStream("xml_content"));
...
}
Bug:14781609 XDB: XMLType.getclobval() returns a temporary LOB when XML is stored in a CLOB.
fix in patchset 11.2.0.4
and another solution
if read as blob, then no error like
T1.PROJECT.getBlobVal(nls_charset_id('UTF8'))
Easy enough to verify if it's the .getClobVal() call or not - perform an INSTR test in PL/SQL (not Java) on your resultant CLOB to see if the CHR(0) exists or not.
If it does not, then I would point the finger at your Oracle client install.

Resources