Selecting conditions dynamically In oracle - oracle

Hi What I would like to do is take the search criterias as parameters to my function and search through the table depending on the input parameters.. for example: IF ALL the parameters are NULL then the function will return evey field in the table, if a parameter is not NULL that parameter will be a search criteria. I could do an If statement but it would be too long and assuming that I would have had a bigger table it would be very painful, Is there any way I could accomplish this Task? Thanks
My Code:
create or replace type ordersinf as object(orderDate DATE, orderName VARCHAR2(20), orderPrice NUMBER)
create or replace type orders_tab IS TABLE OF ordersinf
and My Code is:
create or replace function execImmFunc(orderDate DATE, orderName in
VARCHAR2, orderPrice in NUMBER) return orders_tab is
Result orders_tab := orders_tab();
begin
SELECT ordersinf(orderDate => orders_Orderdate, orderName => orderName,orderPrice => orders_price)
BULK COLLECT INTO RESULT
FROM orders_table
WHERE orders_Orderdate >= orderDate OR orders_name = orderName OR orders_price = orderPrice ;
return(Result);
end execImmFunc;

Something like this:
CREATE OR REPLACE FUNCTION execImmFunc(
orderDate IN DATE,
orderName IN VARCHAR2,
orderPrice IN NUMBER
) RETURN orders_tab
IS
Result orders_tab;
BEGIN
SELECT ordersinf(
orderDate => orders_OrderDate,
orderName => orders_Name,
orderPrice => orders_price
)
BULK COLLECT INTO Result
FROM orders_table
WHERE ( orderDate IS NULL OR orders_OrderDate >= orderDate )
AND ( orderName IS NULL OR orders_name = orderName )
AND ( orderPrice IS NULL OR orders_price = orderPrice );
RETURN Result;
END execImmFunc;

Is this what you mean
WHERE (orderDate is null OR orders_Orderdate >= orderDate) AND (orderName is null OR orders_name = orderName) AND (orderPrice is null OR orders_price = orderPrice );
If a parameter orderDate is null - the clause passes and is effectively ignored?

Hey I dont have workspace with me but below is a very simple snippet
which can be used for dynamic sql formation as per the input provided.
Hope this helps
SET SERVEROUTPUT ON;
DECLARE
lv_col1 VARCHAR2(100):=123;
lv_col2 VARCHAR2(100):=1;
lv_col3 VARCHAR2(100):=NULL;
lv_col4 VARCHAR2(100):=45;
lv_col5 VARCHAR2(100):=8;
lv_sql LONG;
lv_where LONG;
BEGIN
lv_sql:='SELECT * FROM TABLE WHERE 1 = 1 ';
SELECT DECODE(lv_col1,NULL,'',' AND col1 = '''
||lv_col1
||'''')
|| DECODE(lv_col2,NULL,'',' AND col2 = '''
||lv_col2
||'''')
|| DECODE(lv_col3,NULL,'',' AND col3 = '''
||lv_col3
||'''')
|| DECODE(lv_col4,NULL,'',' AND col4 = '''
||lv_col4
||'''')
|| DECODE(lv_col5,NULL,'',' AND col5 = '''
||lv_col5
||'''')
INTO lv_where
FROM dual;
dbms_output.put_line(lv_sql||lv_where);
END;

Are you trying to set up so a parameter is either returned in the resultset or used in the where-clause? It does not look like you do, but rather you want to return all columns and use parameters in where only when they are not null.
If so, why not just a classic nvl-setup?
SELECT ordersinf(orderDate => orders_Orderdate
,orderName => orderName
,orderPrice => orders_price)
BULK COLLECT INTO RESULT
FROM orders_table
WHERE orders_Orderdate >= nvl(orderDate, orders_Orderdate)
OR orders_name = nvl(orderName, orders_name)
OR orders_price = nvl(orderPrice, orders_price);
I imagine those ORs ought to be AND as you probably want all conditions sent in to be true, but that is purely a desugn/requirement issue.

Related

Issue with comma seperated varchar and Number field in oracle

i have written this stored procedure in Oracle:
CREATE OR REPLACE PROCEDURE TMS.SP_BOOKING_CANCEL_SMPL(P_BOOK_TERMINAL NUMBER,
P_BOOK_CODE NUMBER,
P_BOOK_NO VARCHAR2,
P_CANCELLATION_SEATS VARCHAR2,
P_CANCEL_QTY NUMBER,
P_CANCEL_AMOUNT NUMBER,
P_CANCEL_SEAT_QTY NUMBER,
P_SEAT_QTY NUMBER,
P_UNCANCELLED_ID VARCHAR2,
P_UNCANCELLED_QTY NUMBER,
P_CANCEL_TERMINAL NUMBER,
P_CANCEL_SITE NUMBER,
P_CANCEL_SEQ NUMBER,
P_CANCEL_TYPE CHAR,
P_USER_ID VARCHAR2,
P_SYNC CHAR,
P_CREATE_IP VARCHAR2,
P_CREATE_PC VARCHAR2)
IS
d_sql VARCHAR2(32767);
V_CANCEL_CODE NUMBER;
BEGIN
d_sql := 'UPDATE TMS_BOOKD SET BOOKD_CANCEL_YN = ''Y'', BOOKD_CANCEL_DATE = SYSDATE, BOOKD_CANCEL_USER = :UserId, BOOKD_GENDER = NULL
WHERE BOOKD_TERMINAL = :BookDTerminal AND BOOKD_CODE = :BookDCode AND BOOKD_SEAT in (:cancellationIds)';
EXECUTE IMMEDIATE d_sql
USING P_USER_ID, P_BOOK_TERMINAL, P_BOOK_CODE,P_CANCELLATION_SEATS ;
--;
IF P_CANCEL_SEAT_QTY = P_SEAT_QTY
THEN
d_sql := 'UPDATE TMS_BOOKM SET BOOKM_SET_SEATS = NULL, BOOKM_SET_QTY = NULL, BOOKM_SET_AMOUNT = NULL, BOOKM_CANCEL = 1 WHERE BOOKM_TERMINAL = :BookDTerminal
AND BOOKM_CODE = :BookM_Code AND BOOKM_BOOKNO = :BookM_No';
EXECUTE IMMEDIATE d_sql
USING P_BOOK_TERMINAL, P_BOOK_CODE, P_BOOK_NO;
ELSE
d_sql := 'UPDATE TMS_BOOKM SET BOOKM_SET_SEATS = :BOOKM_SET_SEATS, BOOKM_SET_QTY = :BOOK_SET_QTY, BOOKM_CANCEL = 1 WHERE BOOKM_TERMINAL = :BookDTerminal
AND BOOKM_CODE = :BookM_Code AND BOOKM_BOOKNO = :BookM_No';
EXECUTE IMMEDIATE d_sql
USING P_UNCANCELLED_ID, P_UNCANCELLED_QTY, P_BOOK_TERMINAL, P_BOOK_CODE, P_BOOK_NO;
END IF;
END;
/
i am executing this stored procedure with parameter 'P_CANCELLATION_SEATS ' with value: '10,12'
however it throws exceptions: Ora-01722(invalid number) in first query, the issue is most probably with the IN clause comparing Number type column value with string type value in my parameter.
can anyone tell me how can i resolve this issue?
There are few thing which you must keep in mind before running a Proc. First one is whether you need a DYNAMIC SQL . In your case Dynamic SQL is not at all needed. You can directly do it as shown in my code below. Secondly you already identified that you table column BOOKD_SEAT is a NUMBER coulmn and you are trying to compare with a String so its obvious it will through error. You need to pass these values as collection. See below:
You modified code :
--Create a type of Number to hold your input values
CREATE OR REPLACE TYPE var IS TABLE OF NUMBER;
/
CREATE OR REPLACE PROCEDURE TMS.SP_BOOKING_CANCEL_SMPL (
P_BOOK_TERMINAL NUMBER,
P_BOOK_CODE NUMBER,
P_BOOK_NO VARCHAR2,
P_CANCELLATION_SEATS var, -- Declare the input as type of NUMBER
P_CANCEL_QTY NUMBER,
P_CANCEL_AMOUNT NUMBER,
P_CANCEL_SEAT_QTY NUMBER,
P_SEAT_QTY NUMBER,
P_UNCANCELLED_ID VARCHAR2,
P_UNCANCELLED_QTY NUMBER,
P_CANCEL_TERMINAL NUMBER,
P_CANCEL_SITE NUMBER,
P_CANCEL_SEQ NUMBER,
P_CANCEL_TYPE CHAR,
P_USER_ID VARCHAR2,
P_SYNC CHAR,
P_CREATE_IP VARCHAR2,
P_CREATE_PC VARCHAR2)
IS
V_CANCEL_CODE NUMBER;
BEGIN
UPDATE TMS_BOOKD
SET BOOKD_CANCEL_YN = 'Y',
BOOKD_CANCEL_DATE = SYSDATE,
BOOKD_CANCEL_USER = P_USER_ID,
BOOKD_GENDER = NULL
WHERE BOOKD_TERMINAL = P_BOOK_TERMINAL
AND BOOKD_CODE = P_BOOK_CODE
AND BOOKD_SEAT IN (select column_value from table(P_CANCELLATION_SEATS) );
-- Note you can also use MEMBER of operator and change query as
--BOOKD_SEAT MEMBER OF P_CANCELLATION_SEATS
IF P_CANCEL_SEAT_QTY = P_SEAT_QTY
THEN
UPDATE TMS_BOOKM
SET BOOKM_SET_SEATS = NULL,
BOOKM_SET_QTY = NULL,
BOOKM_SET_AMOUNT = NULL,
BOOKM_CANCEL = 1
WHERE BOOKM_TERMINAL = P_BOOK_TERMINAL
AND BOOKM_CODE = P_BOOK_CODE
AND BOOKM_BOOKNO = P_BOOK_NO;
ELSE
UPDATE TMS_BOOKM
SET BOOKM_SET_SEATS = P_UNCANCELLED_ID,
BOOKM_SET_QTY = P_UNCANCELLED_QTY,
BOOKM_CANCEL = 1
WHERE BOOKM_TERMINAL = P_BOOK_TERMINAL
AND BOOKM_CODE = P_BOOK_CODE
AND BOOKM_BOOKNO = P_BOOK_NO;
END IF;
END;
/
Execution:
DECLARE
v_var var := var ();
BEGIN
v_var.EXTEND (2);
--Populate all the values which you want to evalued in IN calsue.
v_var (1) := 1;
v_var (2) := 2;
TMS.SP_BOOKING_CANCEL_SMPL (P_BOOK_TERMINAL => <give your value>
P_BOOK_CODE => <give your value>
P_BOOK_NO => <give your value>
--- pass all the value which you want to be evaluted in IN clause of your query
P_CANCELLATION_SEATS => v_var
P_CANCEL_QTY => <give your value>
P_CANCEL_AMOUNT => <give your value>
P_CANCEL_SEAT_QTY => <give your value>
P_SEAT_QTY => <give your value>
P_UNCANCELLED_ID => <give your value>
P_UNCANCELLED_QTY => <give your value>
P_CANCEL_TERMINAL => <give your value>
P_CANCEL_SITE => <give your value>
P_CANCEL_SEQ => <give your value>
P_CANCEL_TYPE => <give your value>
P_USER_ID => <give your value>
P_SYNC => <give your value>
P_CREATE_IP => <give your value>
P_CREATE_PC => <give your value> )
end;
The parameter P_CANCELLATION_SEAT is a list of comma separated values that you want to pass inside the IN clause, the way you are trying to achieve is technically wrong, you have to break the comma separated valued into list of values before using it in the IN clause.
d_sql := 'UPDATE TMS_BOOKD SET BOOKD_CANCEL_YN = ''Y'', BOOKD_CANCEL_DATE = SYSDATE, BOOKD_CANCEL_USER = :UserId, BOOKD_GENDER = NULL
WHERE BOOKD_TERMINAL = :BookDTerminal AND BOOKD_CODE = :BookDCode AND BOOKD_SEAT in (SELECT to_number(regexp_substr(vlist, ''[^,]+'', 1, LEVEL))
FROM (SELECT :cancellationIds AS vlist FROM dual)
CONNECT BY regexp_substr(vlist, ''[^,]+'', 1, LEVEL) IS NOT NULL)';
BTW, why are you using dynamic SQL? All your operations can be performed using simple SQL statements.

How to return a table of records from a PLSQL function?

I have a function which is having three different queries. First query is returning a single record in a plsql record type.Second query is returning another single value and third is also returning a different single value. Now I want to append first record with those two values and return a table of that new record from my function. How can I achieve that.
create or replace function test(p_actBillDat date) return <what should I return> as
type tab_accountNum is table of account.account_num%type;
var_accountNum tab_accountNum;
type query1Record is record(
accountNum account.account_num%type,
customerRef customer.customer_ref%type,
internalCreditScore CUSTOMERATTRIBUTES.Internal_Credit_Score%type);
var_query1Rec query1Record;
var_nsfDat date;
var_writeOffdat date;
cursor cur_accountNum is
select ACCOUNT_NUM
from BILLSUMMARY
where trunc(ACTUAL_BILL_DTM) = p_actBillDat
and CANCELLATION_REQUEST_DAT is null;
begin
open cur_accountNum;
Loop
fetch cur_accountNum bulk collect
into var_accountNum limit 100;
close cur_accountNum;
for i in 1 .. var_accountNum.count
loop
select A.ACCOUNT_NUM, A.CUSTOMER_REF, CA.INTERNAL_CREDIT_SCORE
into var_query1Rec
from ACCOUNT A, CUSTOMERATTRIBUTES CA, CONTACTDETAILS CD, CONTACT CNT
where A.ACCOUNT_NUM = var_accountNum(i) and
A.CUSTOMER_REF = CA.CUSTOMER_REF(+) and
A.CUSTOMER_REF = CD.CUSTOMER_REF and
CNT.CUSTOMER_REF = A.CUSTOMER_REF and
CD.CONTACT_SEQ = CNT.CONTACT_SEQ and
CD.CONTACT_SEQ = 1 and
CD.START_DAT = (select min(CNTD.START_DAT)
from CONTACTDETAILS CNTD
where CNTD.CONTACT_SEQ = CD.CONTACT_SEQ and
CNTD.CUSTOMER_REF = A.CUSTOMER_REF);
select max(AP.ACCOUNT_PAYMENT_DAT) into var_writeOffdat
from ACCOUNT A, ACCOUNTPAYMENT AP
where A.ACCOUNT_NUM = AP.ACCOUNT_NUM and
A.ACCOUNT_NUM = var_accountNum(i) AND
A.TOTAL_WRITEOFF_TOT <> 0 and
(AP.PAYMENT_ORIGIN_ID = 2 or AP.PAYMENT_ORIGIN_ID = 3) and
AP.CANCELLED_DTM is null and
AP.FAILED_DTM is null;
select max(PP.FAILED_DTM) into var_nsfDat
from ACCOUNTPAYMENT AP, PHYSICALPAYMENT PP
where AP.ACCOUNT_NUM = var_accountNum(i) and
AP.ACCOUNT_PAYMENT_STATUS = 3 and
AP.PHYSICAL_PAYMENT_SEQ = PP.PHYSICAL_PAYMENT_SEQ and
AP.CUSTOMER_REF = PP.CUSTOMER_REF and
PP.PHYSICAL_PAYMENT_STATUS = 3 and
PP.FAILURE_CODE_ID in (select PFC.FAILURE_CODE
from CGPAYMENTFAILURECONFIG PFC
where PFC.FAILURE_TYPE = 'Decline NSF') ;
<how to appned var_query1Rec with var_writeOffdat and var_writeOffdat>
<how to make a PLSQl table of that record and return from function>
end loop;
end loop;
end;
If this function is not part of a package - why wouldn't it be? then you have no other choice but to declare a SQL Object type like this example:
CREATE TYPE person_typ AS OBJECT (
idno NUMBER,
first_name VARCHAR2(20)
);
Declare the variables at the top of your function to access the type created.
type t_arr is table of person_typ ;
l_arr t_arr := t_arr();
Then assign them in your code:
l_arr.extend;
l_arr(i).idno := xxx;
l_arr(i).first_name := yyyy;
The create function returns the object:
create or replace function test(p_actBillDat date) return person_typ as
.....
return(l_arr);
end;
But I would have this function in a package then in the package body header or spec you could do this:
type t_rec is
record(x number
,y varchar2(100)
);
type t_tbl is table of t_rec index by binary_integer;
Then declare in your function:
l_tbl t_tbl;
Then assign them in the function:
l_tbl(i).x := xxx;
l_tbl(i).y := yyy;
And finally just return the type in your function like this:
create or replace function test(p_actBillDat date) return t_tbl as
......
l_tbl t_tbl;
begin
.......
for i in 1..counter loop
.. SQL statements
l_tbl(i).x := xxx;
l_tbl(i).y := yyy;
end loop;
return l_tbl;
end;

How to retrieve distinct values from nested table in Oracle

I have a problem where in I need to retrieve distinct values out of a nested table collection.
Example code:
FUNCTION get_part_atts
( p_gp_id IN dummy_di_parts.di_gp_id%TYPE
, p_attribute_name IN dummy_part_attr_def.attribute_name%TYPE
, p_sel1 IN dummy_di_part_atts.sel1%TYPE DEFAULT NULL
, p_sel2 IN dummy_di_part_atts.sel2%TYPE DEFAULT NULL
, p_sel3 IN dummy_di_part_atts.sel3%TYPE DEFAULT NULL )
RETURN dummy_pkg.part_atts_tabtype
RESULT_CACHE
IS
l_dummy_part_seq dummy_di_parts.dummy_part_seq%TYPE;
l_attribute_id dummy_part_attr_def.attribute_id%TYPE;
l_default_value dummy_part_attr_def.default_value%TYPE;
l_return part_atts_tabtype := part_atts_tabtype();
BEGIN
SELECT p.dummy_part_seq
INTO l_dummy_part_seq
FROM dummy_di_parts p
WHERE p.di_gp_id = p_gp_id
AND p.di_part_status = 'ACTIVE';
EXCEPTION
WHEN NO_DATA_FOUND
THEN RAISE_APPLICATION_ERROR(-20021,'No active parts found '||p_gp_id||'.');
WHEN TOO_MANY_ROWS
THEN RAISE_APPLICATION_ERROR(-20022,'More than one active part '||p_gp_id||'.');
END part_lookup;
BEGIN
SELECT pad.attribute_id
, pad.default_value
INTO l_attribute_id
, l_default_value
FROM dummy_part_attr_def pad
WHERE pad.attribute_name = p_attribute_name;
EXCEPTION
WHEN NO_DATA_FOUND
THEN RAISE_APPLICATION_ERROR(-20023,p_attribute_name||' is not a valid attribute name.');
END attribute_def_lookup;
SELECT pa.attribute_value
BULK COLLECT INTO l_return
FROM dummy_di_part_atts pa
WHERE pa.dummy_part_seq = l_dummy_part_seq
AND pa.attribute_id = l_attribute_id
AND ( p_sel1 IS NULL OR
pa.sel1 = p_sel1 )
AND ( p_sel2 IS NULL OR
pa.sel2 = p_sel2 )
AND ( p_sel3 IS NULL OR
pa.sel3 = p_sel3 );
RETURN l_return;
END get_part_atts;
Now I need to select Distinct from the collection l_return which is a nested table type
Please help
In Oracle 12C you can do this:
select distinct attribute_value
from table (get_part_atts(...))
Prior to 12C you could do similar, but only if the type was defined in the database using CREATE TYPE rather than declared in a package specification.
Try using "multiset union distinct". For example, a function that splits a delimited string into tokens and returns a nested table:
CREATE OR REPLACE function fn_split(i_string in varchar2, i_delimiter in varchar2 default ',', b_dedup_tokens in number default 0)
return sys.dbms_debug_vc2coll
as
l_tab sys.dbms_debug_vc2coll;
begin
select regexp_substr(i_string,'[^' || i_delimiter || ']+', 1, level)
bulk collect into l_tab
from dual
connect by regexp_substr(i_string, '[^' || i_delimiter || ']+', 1, level) is not null
order by level;
if (b_dedup_tokens > 0) then
return l_tab multiset union distinct l_tab;
end if;
return l_tab;
end;
Testing:
select * from table(fn_split('x,x,y,z', ',', 1));
Output:
COLUMN_VALUE
x
y
z

Call to the function failing with : ORA-00904: "TEMP_DATA"."P_LNAME": invalid identifier

The following function compiles fine. But when a call is made to the function in an anonymous PL/SQL code, the call fails.
Can anyone please suggest why ? I am trying to insert the un-matched (new) data and update the matching (existing) data.
Function Fn_Insert_Data_Using_Merge (p_Id in Number,
p_fname in varchar2,
p_mname in varchar2,
p_lname in varchar2,
p_birth_date in date) Return Boolean
Is
Begin
Dbms_Output.put_line ('Inside the function Fn_Insert_Data_Using_Merge ...');
Merge Into test_employee te
using (select distinct p_Id, p_fname, p_mname, p_lname, p_birth_date
from test_employee) temp_data
on (te.first_name = temp_data.p_fname and
te.middle_name = temp_data.p_mname and
te.last_name = temp_data.p_lname)
when matched then
update
set first_name = p_fname,
middle_name = p_mname,
last_name = p_lname,
run_status = 'Updated'
when not matched then
insert (id, first_name, middle_name, last_name, birth_date, run_status)
values
(p_id, p_fname, p_mname, p_lname, p_birth_date, 'Inserted');
Dbms_Output.put_line ('Returning successfully from the function Fn_Insert_Data_Using_Merge ...');
Return True;
Exception
When Dup_Val_On_Index Then
Dbms_Output.put_line ('The name already exists...Cannot insert again ..');
Return False;
When Others Then
Dbms_Output.put_line ('Facing some critical error : ' || SQLERRM);
Return False;
End Fn_Insert_Data_Using_Merge;
The error is trying to tell you that there is no column in your TEMP_DATA extract named P_LNAME. There is a parameter to the function named 'p_lname' but in the extract you've assigned no column aliases so there are no named fields. In addition (although not a cause of the error), doing a DISTINCT from a table with potentially many rows is a slow way to get the function parameters into the temp table in the USING clause; you'd be better off using a SELECT...FROM DUAL. Try replacing your MERGE statement with the following and let us know how it goes:
Merge Into test_employee te
using (select p_Id as p_Id,
p_fname as p_fname,
p_mname as p_mname,
p_lname as p_lname,
p_birth_date as p_birth_date
from dual) temp_data
on (te.first_name = temp_data.p_fname and
te.middle_name = temp_data.p_mname and
te.last_name = temp_data.p_lname)
when matched then
update
set first_name = temp_data.p_fname,
middle_name = temp_data.p_mname,
last_name = temp_data.p_lname,
run_status = 'Updated'
when not matched then
insert (id, first_name, middle_name,
last_name, birth_date, run_status)
values
(temp_data.p_id, temp_data.p_fname, temp_data.p_mname,
temp_data.p_lname, temp_data.p_birth_date, 'Inserted');
Share and enjoy.

Joining columns to rows

Here's a scenario with a product we purchased. THe product allows us to create custom fields, but these fields are stored as ROWS in a CUSTOM table.
I want to write a query that will connect to more than one custom field and fetch a single row.
Let me give you an example.
1) PERSON TABLE (ID int, NAME varchar2(30));
2) CUSTOMFIELDS TABLE(CFID int, CFTable varchar2(30), CFFieldName varchar2(30), CFFieldType varchar2(30));
3) CUSTOMFIELDVALUE TABLE(CFVID int, CFID int, CFFieldName varchar2(100), CFFieldValue varchar2(100));
My person table has one record
1) 1001 - Clark Kent
Let's say I create two custom fields for person called AGE and WEIGHT.
In that case two records will get created in teh CUSTOMFIELDS table.
1) 100 - PERSON - AGE - INTEGER
2) 200 - PERSON - WEIGHT - INTEGER
Now the values for these custom fields will be stored in the CUSTOMFIELDVALUE table like this.
1) 100 - 100 - 1001 - 44
2) 101 - 200 - 1001 - 200 lbs
I want to write a select query that will fetch the record like this
PERSON, AGE , WEIGHT
Clark Kent, 44, 200 lbs
I am thinking how this can be achieved by pure SQL. The number of custom fields can increase or decrease depending on the configuration of the product.
It is an interesting problem. You want to change dynamically number and name of columns. This is not possible to create with "normal" SQL. I tried to create sample with using of PIPELINED FUNCTION.
I first created table:
CREATE TABLE PERSON (ID int, NAME varchar2(30));
CREATE TABLE CUSTOMFIELDS (CFID int, CFTable varchar2(30), CFFieldName varchar2(30), CFFieldType varchar2(30));
CREATE TABLE CUSTOMFIELDVALUE (CFVID int, CFID int, CFFieldName varchar2(100), CFFieldValue varchar2(100));
INSERT INTO PERSON(id, name) values(1001, 'Clark Kent');
INSERT INTO CUSTOMFIELDS(CFID, CFTable, CFFieldName, CFFieldType) values(100, 'PERSON', 'AGE', 'INTEGER');
INSERT INTO CUSTOMFIELDS(CFID, CFTable, CFFieldName, CFFieldType) values(200, 'PERSON', 'WEIGHT', 'INTEGER');
...and I placed some data:
INSERT INTO CUSTOMFIELDVALUE (CFVID, CFID, CFFieldName, CFFieldValue) values(100, 100, 1001, 44);
INSERT INTO CUSTOMFIELDVALUE (CFVID, CFID, CFFieldName, CFFieldValue) values(101, 200, 1001, 200);
Then I created an object type:
CREATE TYPE CustomFieldType AS OBJECT
(
row_id number,
fieldType varchar2(200),
person_id number,
fieldValue1 varchar2(2000),
fieldValue2 varchar2(2000),
fieldValue3 varchar2(2000),
fieldValue4 varchar2(2000),
fieldValue5 varchar2(2000)
)
/
CREATE TYPE CustomFieldTypeSet AS TABLE OF CustomFieldType
/
And also created PIPELINED FUNCTION:
CREATE OR REPLACE
FUNCTION GET_PERSON_FIELDS(person_id_in IN NUMBER
,field_names_in IN VARCHAR2) RETURN CustomFieldTypeSet
PIPELINED
IS
-- constructor CustomFieldType()
l_header_row CustomFieldType := CustomFieldType(NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
l_data_row CustomFieldType := CustomFieldType(NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
l_tablen BINARY_INTEGER;
l_tab DBMS_UTILITY.uncl_array;
l_num_of_field_values PLS_INTEGER := 5;
l_counter PLS_INTEGER := 1;
l_position PLS_INTEGER;
l_field_names_in VARCHAR2(2000) := field_names_in;
TYPE type_header_hash IS TABLE OF PLS_INTEGER INDEX BY VARCHAR2(200);
l_header_hash type_header_hash;
BEGIN
-- 1) check, what fields you can display
IF (l_field_names_in IS NULL) THEN
<<get_all_fields>>
FOR cur_all_fields IN (SELECT DISTINCT flds.CFFIELDNAME
FROM CUSTOMFIELDS flds
,CUSTOMFIELDVALUE cfv
WHERE cfv.CFID = flds.CFID
AND flds.CFTable = 'PERSON' ) LOOP
l_field_names_in := l_field_names_in ||
cur_all_fields.CFFIELDNAME ||
',';
END LOOP get_all_fields;
END IF;
-- 2) generate header (function RTRIM prevent ORA-00931 exception!)
DBMS_UTILITY.comma_to_table(list => RTRIM(l_field_names_in, ','), tablen => l_tablen, tab => l_tab);
l_header_row.row_id := 1;
l_header_row.fieldType := 'HEADER';
<<header_cursor>>
FOR i IN 1..l_tablen LOOP
IF (i = 1) THEN
l_header_row.fieldValue1 := l_tab(i);
l_header_hash(l_tab(i)) := i;
ELSIF (i = 2) THEN
l_header_row.fieldValue2 := l_tab(i);
l_header_hash(l_tab(i)) := i;
ELSIF (i = 3) THEN
l_header_row.fieldValue3 := l_tab(i);
l_header_hash(l_tab(i)) := i;
ELSIF (i = 4) THEN
l_header_row.fieldValue4 := l_tab(i);
l_header_hash(l_tab(i)) := i;
ELSIF (i = 5) THEN
l_header_row.fieldValue5 := l_tab(i);
l_header_hash(l_tab(i)) := i;
END IF;
END LOOP header_cursor;
-- 3) print data to SQL (over pipe)...
PIPE ROW(l_header_row);
FOR cur_persons IN (SELECT ID
FROM PERSON
WHERE ID = COALESCE(person_id_in, ID) ) LOOP
l_data_row.row_id := NULL;
l_data_row.person_id := NULL;
l_data_row.fieldType := NULL;
l_data_row.fieldValue1 := NULL;
l_data_row.fieldValue2 := NULL;
l_data_row.fieldValue3 := NULL;
l_data_row.fieldValue4 := NULL;
l_data_row.fieldValue5 := NULL;
l_data_row.fieldType := 'DATA';
FOR cur_data IN (SELECT p.ID AS person_id
,cfv.CFID
,flds.CFTABLE
,flds.CFFIELDNAME
,cfv.CFFIELDVALUE
FROM PERSON p
,CUSTOMFIELDS flds
,CUSTOMFIELDVALUE cfv
WHERE p.ID = cur_persons.ID
AND p.ID = cfv.CFFIELDNAME
AND cfv.CFID = flds.CFID ) LOOP
l_data_row.person_id := cur_persons.ID;
l_position := NULL;
IF (l_header_hash.EXISTS(cur_data.CFFIELDNAME)) THEN
l_position := l_header_hash(cur_data.CFFIELDNAME);
END IF;
IF (l_position = 1) THEN
l_data_row.fieldValue1 := cur_data.CFFIELDVALUE;
ELSIF (l_position = 2) THEN
l_data_row.fieldValue2 := cur_data.CFFIELDVALUE;
ELSIF (l_position = 3) THEN
l_data_row.fieldValue3 := cur_data.CFFIELDVALUE;
ELSIF (l_position = 4) THEN
l_data_row.fieldValue4 := cur_data.CFFIELDVALUE;
ELSIF (l_position = 5) THEN
l_data_row.fieldValue5 := cur_data.CFFIELDVALUE;
END IF;
END LOOP;
l_counter := l_counter + 1;
l_data_row.row_id := l_counter;
PIPE ROW(l_data_row);
END LOOP;
RETURN;
END GET_PERSON_FIELDS;
Than you can use SQL to get sample data (note: prevent exception ORA-22905, you must set session variable "ALTER SESSION SET CURSOR_SHARING=EXACT;"):
SELECT * FROM TABLE(GET_PERSON_FIELDS(1001,'AGE,WEIGHT'));
And here is output:
ROW_ID FIELDTYPE PERSON_ID FIELDVALUE FIELDVALUE FIELDVALUE
------ ---------- --------- ---------- ---------- ----------
1 HEADER AGE
2 DATA 1001 44
In the first column is header, where is stored information about field names and after header are stored data. You can use combination of these SQLs:
SELECT * FROM TABLE(GET_PERSON_FIELDS(1001,'AGE,WEIGHT'));
SELECT * FROM TABLE(GET_PERSON_FIELDS(1002,'AGE,GENDER'));
SELECT * FROM TABLE(GET_PERSON_FIELDS(1001,NULL));
SELECT * FROM TABLE(GET_PERSON_FIELDS(NULL,NULL));
First argument is person_id
Second argument is list of items that you want to see the output
if first argument is NULL, you can see list of all persons
if second argument is NULL, you can see list of all arguments
Script is not complete, and have some limitations:
in object CustomFieldType you cannot dynamically change number of fields (I mean fieldValue1, fieldValue2...)
in body of function GET_PERSON_FIELDS as you can see, dynamically problem is also in IF statements (IF (l_position = 1) THEN l_data_row.fieldValue1, IF (l_position = 2) THEN l_data_row.fieldValue1)...
Finally, when I entered some sample data, like this:
INSERT INTO PERSON(id, name) values(1002, 'Lois Lane');
INSERT INTO CUSTOMFIELDS(CFID, CFTable, CFFieldName, CFFieldType) values(300, 'PERSON', 'GENDER', 'VARCHAR');
INSERT INTO CUSTOMFIELDS(CFID, CFTable, CFFieldName, CFFieldType) values(400, 'PERSON', 'SINGLE', 'VARCHAR');
INSERT INTO CUSTOMFIELDVALUE (CFVID, CFID, CFFieldName, CFFieldValue) values(102, 100, 1002, 45);
INSERT INTO CUSTOMFIELDVALUE (CFVID, CFID, CFFieldName, CFFieldValue) values(103, 300, 1002, 'FEMALE');
INSERT INTO CUSTOMFIELDVALUE (CFVID, CFID, CFFieldName, CFFieldValue) values(104, 400, 1002, 'YES');
... and ran this SQL command:
SELECT * FROM TABLE(GET_PERSON_FIELDS(NULL,NULL));
... output looked like this:
ROW_ID FIELDTYPE PERSON_ID FIELDVALUE FIELDVALUE FIELDVALUE FIELDVALUE
------ ---------- --------- ---------- ---------- ---------- ----------
1 HEADER AGE GENDER SINGLE WEIGHT
2 DATA 1001 44 200
3 DATA 1002 45 FEMALE YES
Something like this perhaps:
select p.name, age.CFFieldValue, weight.CFFieldValue
from person p
inner join CUSTOMFIELDVALUE age
on p.id = age.CFFieldName
inner join CUSTOMFIELDS age_f
on age_f.cfid = age.cfid and age_f.CFFieldName = 'AGE' and age_f.CFTable = 'PERSON'
inner join CUSTOMFIELDVALUE weight
on p.id = weight.CFFieldName and weight_f.CFFieldName = 'AGE' and weight_f.CFTable = 'PERSON'
inner join CUSTOMFIELDS weight_f
on weight_f.cfid = weight.cfid;
Its not completely clear to me that person.id joins to customfieldvalue.cfieldname but you do say 100 - 100 - 1001 - 44 so I guess you are trying to say 1001 is the third column? Essentially the query filters the customfieldvalues table and uses it as two separate tables. Which I think is what you want - you question doesn't make it very clear.

Resources