In the source code if I want to find out something, say where a particular column is getting updated. I am using the below query to find where the column(BUCKET) of table(LEA_AGREEMENT_DTL)is getting updated.
select * from user_source where upper(text) like '%UPDATE%LEA_AGREEMENT_DTL%BUCKET%';
Now if the source in that paticular schem is written as below my query will tell me in which procedure the code is written :
Update Lea_agreement_dtl Set Dpd = No_Days_OverDuE, bucket=V_bucket
where ProposalID = T_ProposalID;
But if the code is written as below my query will not give any result :
Update Lea_agreement_dtl
Set Dpd = No_Days_OverDuE,
bucket=V_bucket
where ProposalID = T_ProposalID;
My question is how should I modify my query to search in the source code of a particular schema/user so that I get successful result in both the cases. Basically I want to find all the procedure's/objects where this column is getting updated in that particular schema.
As per my analysis if the whole query is written in one line then i am able to search it otherwise it doesn't work.
You can use this view.
CREATE OR REPLACE VIEW V_UPDATE_USER_SOURCE AS
SELECT TYPE,
NAME,
line,
LISTAGG(regexp_replace(text,'[[:space:]]',' '), ' ') WITHIN GROUP(ORDER BY norder) text
FROM (SELECT a.TYPE,
a.NAME,
a.line,
b.text,
b.line norder
FROM (SELECT line,
TYPE,
NAME,
text
FROM user_source
WHERE regexp_like(text, '[[:space:]]update[[:space:]]', 'i')) a,
user_source b
WHERE a.type = b.TYPE
AND a.name = b.name
AND b.line BETWEEN a.line AND (SELECT MIN(g.line)
FROM user_source g
WHERE regexp_like(g.TEXT, ';', 'i')
AND g.TYPE = a.TYPE
AND g.NAME = a.name
AND g.line >= a.line)
ORDER BY b.TYPE,
b.NAME,
b.line) c
GROUP BY TYPE,
NAME,
line
LISTAGG working on Oracle 11g or higher.
If you using previous version Oracle, you need this view
CREATE OR REPLACE VIEW V_UPDATE_USER_SOURCE AS
SELECT TYPE,
NAME,
line,
XMLAGG(XMLELEMENT(E, regexp_replace(text, '[[:space:]]', ' ') || ' ')).EXTRACT('//text()') text
FROM (SELECT a.TYPE,
a.NAME,
a.line,
b.text,
b.line norder
FROM (SELECT line,
TYPE,
NAME,
text
FROM user_source
WHERE regexp_like(text, '[[:space:]]update[[:space:]]', 'i')) a,
user_source b
WHERE a.type = b.TYPE
AND a.name = b.name
AND b.line BETWEEN a.line AND (SELECT MIN(g.line)
FROM user_source g
WHERE regexp_like(g.TEXT, ';', 'i')
AND g.TYPE = a.TYPE
AND g.NAME = a.name
AND g.line >= a.line)
ORDER BY b.TYPE,
b.NAME,
b.line)
GROUP BY TYPE,
NAME,
line;
It returns the data set, which contains all the update is in line with a number, in which the update started.
View can be used with regexp_like:
select * from V_UPDATE_USER_SOURCE t where regexp_like(text,'clients.*STATE','i')
Description:
clients - table.
STATE - column for update.
There are 3 main approaches to this problem:
Hack something together with string functions and regular expressions. This is the most common solution but is also full of holes. Those holes may not matter depending on why you're searching for the code. This is good enough for finding where to start coding but is probably not sufficient for proving to someone that the code is covered.
Tokenize the input and loop through the tokens looking for a pattern. A lexer can take care of some problems that are not easily handled by single regular expressions, like comments and the alternative quoting syntax. With a good understanding of Oracle syntax it's possible to build a highly-accurate solution. It would still miss extremely broken code (for example, it's possible to create code with UPDATE as a variable), dynamic SQL, and recursive references through synonyms and views.
Parse the input and walk an abstract syntax tree looking for a pattern. This would be a nice 100%-accurate solution. Unfortunately there are no high-quality PL/SQL parsers available in PL/SQL.
The solution below uses an open-source PL/SQL lexer I created to solve problems like this.
Sample Procedure
The string pattern, UPDATE-LEA_AGREEMENT_DTL-BUCKET, occurs three times below. But only the first UPDATE on line 4 really satisfies the conditions. (The third update may look weird in the Stackoverflow syntax highlighting, but in real PL/SQL that second quotation mark does not close the string.)
create or replace procedure search_test is
begin
--Real match.
Update Lea_agreement_dtl Set Dpd = No_Days_OverDuE,
bucket=V_bucket
where ProposalID = T_ProposalID;
--Exclude - ignore comments.
/*Update Lea_agreement_dtl Set Dpd = No_Days_OverDuE, bucket=V_bucket
where ProposalID = T_ProposalID;*/
--Exclude - watch out for alternative quoting syntax.
Update Lea_agreement_dtl Set Dpd = q'[No_Days_OverDuE', bucket=V_bucket ]'
where ProposalID = T_ProposalID;
end;
/
Anonymous Block to Find the Lines
--Find lines that update LEA_AGREEMENT_DTL.BUCKET.
--Does not search dynamic SQL and does not recurse through view or synonyms.
declare
v_tokens token_table;
v_update_line_number number;
v_has_update boolean := false;
v_has_table boolean := false;
v_has_column boolean := false;
begin
--Get source and tokenize it.
v_tokens := tokenizer.tokenize(dbms_metadata.get_ddl('PROCEDURE', 'SEARCH_TEST'));
--Loop through tokens and look for UPDATE/LEA_AGREEMENT_DTL/BUCKET between semicolons.
for i in 1 .. v_tokens.count loop
--Reset if a semicolon is found.
if v_tokens(i).value = ';' then
v_has_update := false;
v_has_table := false;
v_has_column := false;
--Look for relevant tokens, in order.
elsif upper(v_tokens(i).value) = 'UPDATE' then
v_update_line_number := v_tokens(i).line_number;
v_has_update := true;
elsif v_has_update and upper(v_tokens(i).value) = 'LEA_AGREEMENT_DTL' then
v_has_table := true;
elsif v_has_table and upper(v_tokens(i).value) = 'BUCKET' then
v_has_column := true;
end if;
--Success if all conditions are met.
if v_has_update and v_has_table and v_has_column then
--Subtract 1 because DBMS_METADATA put a blank line at the beginning.
dbms_output.put_line('Found on line '||to_char(v_update_line_number-1));
v_has_update := false;
v_has_table := false;
v_has_column := false;
end if;
end loop;
end;
/
Results
Found on line 4
Related
I am executing a query in PL / SQL in version 7 and version 14, with a function created by me, and both bring me some results, the rest bring 0.
However, when executing the same query in Oracle SQL Developer, the query brings all the results correctly.
I executed the procedure through PL / SQL and Oracle SQL Developer as well, but then none brought me the right result, all the lines were left as "0".
I can't find the problem at all, even on Google.
Basically, the function multiplies the number of rows by columns that start with "ID_", as shown below.
Function:
CREATE OR REPLACE FUNCTION DS_FUNCESP.FNBIGB_CheckDataCells
(pOwn IN VARCHAR2,
pTab IN VARCHAR2)
RETURN NUMBER
IS
v_Qtd NUMBER;
v_str VARCHAR2(2000);
BEGIN
v_Qtd := 1;
v_str := ' SELECT
SUM((SELECT COUNT(1) AS QTY_ROWS FROM ' || pOwn || '.' || pTab || ' d WHERE d.LINORIGEM <> ''CARGA MANUAL'')) AS QTY_DATA
FROM DW_FUNCESP.D_BI_COLUMNS a
LEFT JOIN
DW_FUNCESP.D_BI_TABLES b
ON a.ID_TABLE = b.ID_TABLE
AND a.ID_OWNER = b.ID_OWNER
LEFT JOIN DW_FUNCESP.D_BI_OWNERS c
ON a.ID_OWNER = c.ID_OWNER
WHERE b.NM_TABLE = ''' || pTab || '''
AND a.IN_PRIMARYKEY = ''NAO''
AND SUBSTR(a.NM_COLUMN,1,3) = ''ID_'' ';
DBMS_OUTPUT.put_line(v_str);
EXECUTE IMMEDIATE v_str into v_Qtd ;
return (v_Qtd);
EXCEPTION WHEN OTHERS THEN
RETURN 0;
END FNBIGB_CheckDataCells;
Select statement:
SELECT
c.NM_OWNER ,
b.NM_TABLE ,
DS_FUNCESP.FNBIGB_CHECKDATACELLS(c.NM_OWNER, b.NM_TABLE) AS QTY_DATA
FROM DW_FUNCESP.D_BI_TABLES b
LEFT JOIN DW_FUNCESP.D_BI_OWNERS c
ON b.ID_OWNER = c.ID_OWNER;
Results from PL/SQL:
Results from Oracle SQL Developer:
Clearly we can see the difference from any row, the right one is the Oracle SQL Developer. So I'd like to know what is the problem, how to fix, because the procedure is adding "0" to all the rows, no matter where I run.
Reading those examples from WHEN OTHERS - A Bug, thanks to #Lalit Kumar B for that, I changed:
EXCEPTION WHEN OTHERS THEN
RETURN 0;
To:
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('SQLCODE: '||SQLCODE);
DBMS_OUTPUT.PUT_LINE('Message: '||SQLERRM);
RAISE;
To find out the problem, and thanks for that I found that it was trying to count from a table where it doesn't exist anymore.
So I using an error handling as below, from #Jeffrey Kemp
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -942 THEN
RAISE;
END IF;
Also, thanks for #Belayer, my code was the problem, agreed on that. Also, executing on both softwares, made me even more confused. I'll read also that documentation for sure.
I created the main query that returns values for a whole, with 2 secondary conditions to restrict choice to be taken in the side combo box.
Everything works with the set parameters. I wish I could turn off or turn on these conditions with side combo box, how should I proceed?
my code is in Delphi:
procedure TForm1.Button3Click(Sender: TObject);
begin
FDQuery3.Close;
FDquery3.Params[0].Value := Datetimepicker1.Date;
FDquery3.Params[1].Value := Datetimepicker2.Date;
FDQuery3.Params[2].Value := Combobox3.Items [Combobox3.Itemindex];
FDQuery3.Params[3].Value := Combobox5.Items [Combobox5.Itemindex];
FDQuery3.Open;
end;
SQL text is:
select
G.NUM_PROG,T.DATA,T.ORA,C.DESCRIZIONE,
(select DESKEY from ANAFORN where CODKEY=T.CODICE ) as Cliente,
O.NOMINATIVO, T.TERMINALE,T.INCASSO
from LG_RIGHE G
inner join LG_TESTA T on G.NUM_PROG =T.NUM_PROG
inner join OPERATORI O on T.OPERATORE = O.CODICE
inner join LG_CAUSA C on T.CAUSALE = C.CODICE
where T.DATA >= :data1
and T.DATA <= :data2
and T.INCASSO = :pagamento
and T.TERMINALE = :terminale
order by G.NUM_PROG
i want turn on/off only Params[2][ and Params[3] (name: pagamento, terminale)
1) The typical way to optionally ignore a condition is to add one more "toggle" parameter. Consider this Delphi code
Result := True; // or False. Actually - some previous part of formula
If Need_Check_Option_A then
Result := Result and ( Option_A > 20 );
If Need_Check_Option_B then
Result := Result and ( Option_B < 10 );
Got the idea?
But very long that is, is there a more concise way to write it ?
Result := .....some other parts....
and (Ignore_Option_A or (Option_A > 20 ))
and (Ignore_Option_B or (Option_A < 10 ))
and ....
Now let's re-phrase it from Delphi to SQL WHERE clause
WHERE (.......) and (......)
AND ( ( :Use_pagamento = 0 ) or ( T.INCASSO = :pagamento ) )
AND ( ( :Use_terminale = 0 ) or ( T.TERMINALE = :terminale ) )
Whether you set that USE_xxxx parameter to zero (similar to false) then the second check would be shortcut out, ignored.
And the calling code would be something like
FDquery3.ParamByName('data1').AsDate := Datetimepicker1.Date;
FDquery3.ParamByName('data2').AsDate := Datetimepicker2.Date;
FDQuery3.ParamByName('pagamento').AsString := Combobox3.Items [Combobox3.Itemindex];
FDQuery3.ParamByName('terminale').AsString := Combobox5.Items [Combobox5.Itemindex];
FDQuery3.ParamByName('Use_pagamento').AsSmallInt := Ord( CheckBox3.Checked );
FDQuery3.ParamByName('Use_terminale').AsSmallInt := Ord( CheckBox5.Checked );
Some more suggestions follow:
2) using names like ComboBox3 are bad. You would not understand what they mean, what was they intended to be for. Look at your SQL - you give names there! You do not make it like
SELECT FIELD1, FIELD2 FROM TABLE1 WHERE FIELD3 < :PARAM1
And you have to give reasonable names to your Delphi objects too!
That FDQuery3, that Checkbox3 that Combobox5 - rename them all, give them some meaningful names!
3) you have a nested select there as the Cliente column. Unless very special circumstances that is slow and inefficient - change it to JOIN too (maybe to LEFT JOIN, if sometimes there is no matching value)
select
G.NUM_PROG,T.DATA,T.ORA,C.DESCRIZIONE,
-- (select DESKEY from ANAFORN where CODKEY=T.CODICE ) as Cliente,
A.DESKEY as Cliente,
O.NOMINATIVO, T.TERMINALE,T.INCASSO
from LG_RIGHE G
inner join LG_TESTA T on G.NUM_PROG =T.NUM_PROG
inner join OPERATORI O on T.OPERATORE = O.CODICE
inner join LG_CAUSA C on T.CAUSALE = C.CODICE
/* left */ join ANAFORN A on A.CODKEY=T.CODICE
where T.DATA >= :data1
and T.DATA <= :data2
AND ( ( :Use_pagamento = 0 ) or ( T.INCASSO = :pagamento ) )
AND ( ( :Use_terminale = 0 ) or ( T.TERMINALE = :terminale ) )
order by G.NUM_PROG
4) Depending on the circumstances you may just want to alter the SQL text.
If the parameter would be ignored - then simply remove it!
This option is not universal, it has good and bad sides though.
But in your case it would rather do good or nothing - because you have human to re-open the query and human would not be able to do it more often than once per second.
Good: then the server gets your SQL text it prepares the QUERY PLAN. The internal program of how to fetch your data. And it does not know yet what your parameters would be, so it prepares the PLAN to always check those parameters. Even if you later would ignore them. Sometimes it might make server choose slow PLAN where it could choose faster one if it knew the parameter would be not used. Sometimes it would make no difference. Game of luck.
Bad: if you keep the SQL text the same, then you can PREPARE the query once and the server would not build different PLAN when you re-open the query with different parameters. But if you do change the SQL text, then server would have to parse that new query and PREPARE the PLAN again before it would give you data. Sometimes it would take considerable time when you open-close queries, say, 1000 times per second. OF course, when you use a human to set those checkboxes, comboboxes and then press buttons, he would not do it that frequently, so in this case that risk is moot.
So in your case you might do something like this instead of introducing those toggle-parameters:
var qt: TStrings; // SQL query text
.....
qt := FDQuery3.SQL;
qt.Clear; // changing SQL Text would auto-close the query
qt.Add('select G.NUM_PROG,T.DATA,T.ORA,C.DESCRIZIONE, ');
qt.Add(' A.DESKEY as Cliente, O.NOMINATIVO, T.TERMINALE,T.INCASSO ');
qt.Add('from LG_RIGHE G ');
qt.Add(' join LG_TESTA T on G.NUM_PROG = T.NUM_PROG ');
qt.Add(' left join ANAFORN A on A.CODKEY=T.CODICE');
qt.Add(' join OPERATORI O on T.OPERATORE = O.CODICE ');
qt.Add(' join LG_CAUSA C on T.CAUSALE = C.CODICE ');
qt.Add('where T.DATA >= :data1 and T.DATA <= :data2 ');
if CheckBox3.Checked then
qt.Add(' and T.INCASSO = :pagamento ');
if CheckBox5.Checked then
qt.Add(' and T.TERMINALE = :terminale ');
qt.Add('order by G.NUM_PROG');
FDquery3.ParamByName('data1').AsDate := Datetimepicker1.Date;
FDquery3.ParamByName('data2').AsDate := Datetimepicker2.Date;
if CheckBox3.Checked then
FDQuery3.ParamByName('pagamento').AsString := Combobox3.Items [Combobox3.Itemindex];
if CheckBox3.Checked then
FDQuery3.ParamByName('terminale').AsString := Combobox5.Items [Combobox5.Itemindex];
FDQuery3.Open;
In this option you do not introduce extra toggle-parameters, but instead you only add value-parameters when user checked to use them. If user unchecked them - then you do not include them into your SQL text and consequently you do not assign them any values (they would not be found anyway).
5) you may use BETWEEN - it may be easier to read.
...
where ( T.DATA BETWEEN :data1 AND :data2 )
and T.INCASSO = :pagamento
....
I have the following function
CREATE OR REPLACE FUNCTION match_custom_filter(filters text[], id text)
RETURNS boolean LANGUAGE plpgsql as $$
DECLARE
r boolean;
BEGIN
execute format(
'SELECT 1 FROM trackings t LEFT JOIN visitors v ON v.id = t.visitor_id
WHERE v.id = ''%s'' AND %s',
id,
array_to_string(filters, ') AND ('))
into r;
RETURN r;
END $$;
select v.*, array_agg(g.name) as groups from visitors v join groups g on match_custom_filter(g.formatted_custom_filters, v.id)
where v.id = 'cov4pisw00000sjctfyvwq126'
group by v.id
This works fine when the filters are not empty. But it is also possible that a filter is empty, in which case I will have an dangling AND with no right hand side.
Error:
ERROR: syntax error at end of input
LINE 2: ... WHERE v.id = 'cov4pisw00000sjctfyvwq126' AND
^
QUERY: SELECT 1 FROM trackings t LEFT JOIN visitors v ON v.id = t.visitor_id
WHERE v.id = 'cov4pisw00000sjctfyvwq126' AND
CONTEXT: PL/pgSQL function match_custom_filter(text[],text) line 5 at EXECUTE statement
What's the best way to handle this?
UPDATE:
Example of how I generate the array of string filters based off JSONB array of filter objects
def build_condition(%{"filter" => filter, "field" => field, "value" => value}) when field in #default_values do
case filter do
"greater_than" -> "#{field} > #{value}"
"less_than" -> "#{field} < #{value}"
"is" -> "#{field} = '#{value}'"
"is_not" -> "#{field} <> '#{value}'"
..
First, a warning. What you are doing here gives you in-stored-proc sql injection. I highly recommend you reconsider so you can properly parameterize.
Now, having said this, the obvious option is to declare a text variable and then pre-process it.
In your DECLARE block you add:
filterstring text;
then in your body, you add:
filterstring := array_to_string(filters, ') AND ('))
IF filterstring = '' or filterstring is null THEN
filterstring := 'TRUE';
END IF;
Then you use filterstring in place of the array_to_string call in the format() call.
Note that any time you assemble a query anywhere by string interpolation you have the possibility of sql injection.
To protect against SQL injection you will need to rethink your approach a little bit. Your best option is not to use format() for your query to the extent possible. So:
execute 'SELECT 1 FROM trackings t
LEFT JOIN visitors v ON v.id = t.visitor_id
WHERE v.id = $1'
USING id;
That causes planning and filling in the value to happen on two different points. That works well in the case of a simple parameter. However it doesn't work well in the case of the dynamic filters.
Instead of passing a one-dimensional array in, you could pass a two dimensional (nx3 array) with three elements per line. These would be column name, operator, and value. You can sanitize the column name by passing it through quote_ident and the value by passing it through quote_literal but sanitizing the operators is likely to be a problem so my recommendation would be to whitelist these and throw an exception if the operator is not found. Something like:
DECLARE
...
op TEXT;
allowed_ops TEXT[] := ARRAY['=', '<=', '>='];
BEGIN
...
IF not(op = ANY(allowed_ops)) THEN
RAISE EXCEPTION 'Illegal operator in function, %', op;
END IF;
...
END;
This is not going to be easy but it is doable.
Since you have your filters in the form of a jsonb array to begin with, you should use that as a function parameter instead of a text[]. For one thing, it will allow you to protect against SQL-injection.
CREATE OR REPLACE FUNCTION match_custom_filter(filters jsonb, id text)
RETURNS boolean LANGUAGE plpgsql AS $$
DECLARE
f text;
r boolean;
BEGIN
IF jsonb_array_length(filters) = 0 THEN
-- If no filters are specified then run a straight SQL query against trackings
PERFORM * FROM trackings WHERE visitor_id = quote_literal(id);
RETURN FOUND;
ELSE
-- Build the filters from the jsonb array
SELECT string_agg(
-- Concatenate the parts from a single json object into a filter
quote_ident(j->>'field') || -- avoid SQL injection on column name
CASE j->>'type'
WHEN 'greater_than' THEN ' > '
...
END ||
quote_literal(j->>'value'), -- avoid SQL injection on value
-- Aggregate individual filters with the AND operator
' AND ') INTO f
FROM jsonb_array_elements(filters) j;
-- Run a dynamic query with the filters
EXECUTE format('SELECT true FROM trackings t
LEFT JOIN visitors v ON v.id = t.visitor_id
WHERE v.id = %L AND %s LIMIT 1', id, f) INTO r;
RETURN r;
END IF;
END $$;
You should call this function passing in the jsonb array, like so:
SELECT v.*, array_agg(g.name) AS groups
FROM visitors v JOIN groups g ON match_custom_filter(g.group->'filter', v.id)
WHERE v.id = 'cov4pisw00000sjctfyvwq126'
GROUP BY v.id;
I have below SQL as a part of a view. In one of the schema I am getting "String Concatenation is too long" error and not able to execute the view.
Hence I tried the TO_CLOB() and now VIEW is not throwing ERROR, but it not returning the result as well it keep on running..
Please suggest....
Sql:
SELECT Iav.Item_Id Attr_Item_Id,
LISTAGG(La.Attribute_Name
||'|~|'
|| Lav.Attribute_Value
||' '
|| Lau.Attribute_Uom, '}~}') WITHIN GROUP (
ORDER BY ICA.DISP_SEQ,LA.ATTRIBUTE_NAME) AS ATTR
FROM Item_Attribute_Values Iav,
Loc_Attribute_Values Lav,
Loc_Attribute_Uoms Lau,
Loc_Attributes La,
(SELECT *
FROM Item_Classification Ic,
CATEGORY_ATTRIBUTES CA
WHERE IC.DEFAULT_CATEGORY='Y'
AND IC.TAXONOMY_TREE_ID =CA.TAXONOMY_TREE_ID
) ICA
WHERE IAV.ITEM_ID =ICA.ITEM_ID(+)
AND IAV.ATTRIBUTE_ID =ICA.ATTRIBUTE_ID(+)
AND Iav.Loc_Attribute_Id =La.Loc_Attribute_Id
AND La.Locale_Id =1
AND Iav.Loc_Attribute_Uom_Id =Lau.Loc_Attribute_Uom_Id(+)
AND Iav.Loc_Attribute_Value_Id=Lav.Loc_Attribute_Value_Id
GROUP BY Iav.Item_Id;
Error:
ORA-01489: result of string concatenation is too long
01489. 00000 - "result of string concatenation is too long"
*Cause: String concatenation result is more than the maximum size.
*Action: Make sure that the result is less than the maximum size.
You can use the COLLECT() function to aggregate the strings into a collection and then use a User-Defined function to concatenate the strings:
Oracle Setup:
CREATE TYPE stringlist IS TABLE OF VARCHAR2(4000);
/
CREATE FUNCTION concat_List(
strings IN stringlist,
delim IN VARCHAR2 DEFAULT ','
) RETURN CLOB DETERMINISTIC
IS
value CLOB;
i PLS_INTEGER;
BEGIN
IF strings IS NULL THEN
RETURN NULL;
END IF;
value := EMPTY_CLOB();
IF strings IS NOT EMPTY THEN
i := strings.FIRST;
LOOP
IF i > strings.FIRST AND delim IS NOT NULL THEN
value := value || delim;
END IF;
value := value || strings(i);
EXIT WHEN i = strings.LAST;
i := strings.NEXT(i);
END LOOP;
END IF;
RETURN value;
END;
/
Query:
SELECT Iav.Item_Id AS Attr_Item_Id,
CONCAT_LIST(
CAST(
COLLECT(
La.Attribute_Name || '|~|' || Lav.Attribute_Value ||' '|| Lau.Attribute_Uom
ORDER BY ICA.DISP_SEQ,LA.ATTRIBUTE_NAME
)
AS stringlist
),
'}~}'
) AS ATTR
FROM your_table
GROUP BY iav.item_id;
LISTAGG is limited to 4000 characters unfortunately. So you may want to use another approach to concatenate the values.
Anyway ...
It is strange to see LISTAGG which is a rather new feature combined with error-prone SQL1992 joins. I'd suggest you re-write this. Are the tables even properly joined? It looks strange that there seems to be no relation between Loc_Attributes and, say, Loc_Attribute_Values. Doesn't have Loc_Attribute_Values a Loc_Attribute_Id so an attribute value relates to an attribute? It would be hard to believe that there is no such relation.
Moreover: Is it guaranteed that your classification subquery doesn't return more than one record per attribute?
Here is your query re-written:
select
iav.item_id as attr_item_id,
listagg(la.attribute_name || '|~|' || lav.attribute_value || ' ' || lau.attribute_uom,
'}~}') within group (order by ica.disp_seq, la.attribute_name) as attr
from item_attribute_values iav
join loc_attribute_values lav
on lav.loc_attribute_value_id = iav.loc_attribute_value_id
and lav.loc_attribute_id = iav.loc_attribute_id -- <== maybe?
join loc_attributes la
on la.loc_attribute_id = lav.loc_attribute_id
and la.loc_attribute_id = lav.loc_attribute_id -- <== maybe?
and la.locale_id = 1
left join loc_attribute_uoms lau
on lau.loc_attribute_uom_id = iav.loc_attribute_uom_id
and lau.loc_attribute_id = iav.loc_attribute_id -- <== maybe?
left join
(
-- aggregation needed to get no more than one sortkey per item attribute?
select ic.item_id, ca.attribute_id, min (ca.disp_seq) as disp_seq
from item_classification ic
join category_attributes ca on ca.taxonomy_tree_id = ic.taxonomy_tree_id
where ic.default_category = 'y'
group by ic.item_id, ca.attribute_id
) ica on ica.item_id = iav.item_id and ica.attribute_id = iav.attribute_id
group by iav.item_id;
Well, you get the idea; check your keys and alter your join criteria where necessary. Maybe this gets rid of duplicates, so LISTAGG has to concatenate less attributes, and maybe the result even stays within 4000 characters.
Xquery approach.
Creating extra types or function isn't necessary.
with test_tab
as (select object_name
from all_objects
where rownum < 1000)
, aggregate_to_xml as (select xmlagg(xmlelement(val, object_name)) xmls from test_tab)
select xmlcast(xmlquery('for $row at $idx in ./*/text() return if($idx=1) then $row else concat(",",$row)'
passing aggregate_to_xml.xmls returning content) as Clob) as list_in_lob
from aggregate_to_xml;
I guess you need to write a small function to concatenate the strings into a CLOB, because even when you cast TO_CLOB() the LISTAGG at the end, this might not work.
HereĀ“s a sample-function that takes a SELECT-Statement (which MUST return only one string-column!) and a separator and returns the collected values as a CLOB:
CREATE OR REPLACE FUNCTION listAggCLob(p_stringSelect VARCHAR2
, p_separator VARCHAR2)
RETURN CLOB
AS
cur SYS_REFCURSOR;
s VARCHAR2(4000);
c CLOB;
i INTEGER;
BEGIN
dbms_lob.createtemporary(c, FALSE);
IF (p_stringSelect IS NOT NULL) THEN
OPEN cur FOR p_stringSelect;
LOOP
FETCH cur INTO s;
EXIT WHEN cur%NOTFOUND;
dbms_lob.append(c, s || p_separator);
END LOOP;
END IF;
i := length(c);
IF (i > 0) THEN
RETURN dbms_lob.substr(c,i-length(p_separator));
ELSE
RETURN NULL;
END IF;
END;
This function can be used f.e. like this:
WITH cat AS (
SELECT DISTINCT t1.category
FROM lookup t1
)
SELECT cat.category
, listAggCLob('select t2.name from lookup t2 where t2.category = ''' || cat.category || '''', '|') allcategorynames
FROM cat;
So i'm in the middle of testing the accuracy of my normalized data by creating a tool to de-normalize the data for comparison. While doing this i was looking into learning new techniques for this tool over what i normally would do(which was using cursors and looping through an insert/update) So i came across two items i wanted to try which were bulk collections and the merge statement. My problem is i'm having some trouble finding the best way to utilize the bulk collection.
EDIT:
Okay so i found my problem/solution when it came to the bulk collection. It was in fact the way i was fetching it. Instead of using the forall statement i changed it to a for and added a loop underneath it. Which lead to the discoveries of more bugs. The way i was trying to call the values stored in the indx was being done wrong so i've rectified that. Now the only problem i seem to be having is with the error noted in the title. In my merge for some reason the first value i try to use in the insert throws the following error:
PL/SQL: ORA-38101: Invalid column in the INSERT VALUES Clause: "TMI"."MACHINE_INTERVAL_ID"
ORA-06550: line 92, column 7:
So what i would like to know now is why exactly i'm getting this error. I understand the concept that my insert value is invalid. but i do not fully understand why this is so.
This is the merge statement in question:
MERGE INTO TEST_MACHINE_INTERVAL TMI
USING (SELECT * FROM TEST_MACHINE_INTERVAL) OTMI
ON (TMI.MACHINE_INTERVAL_ID = OTMI.MACHINE_INTERVAL_ID)
WHEN MATCHED THEN
UPDATE SET TMI.MACHINE_INTERVAL_ID = OTMI.MACHINE_INTERVAL_ID, TMI.START_DATE_TIME = OTMI.START_DATE_TIME,
TMI.INTERVAL_DURATION = OTMI.INTERVAL_DURATION, TMI.CALC_END_TIME = OTMI.CALC_END_TIME,
TMI.MACHINE_NAME = OTMI.MACHINE_NAME, TMI.SITE_NAME = OTMI.SITE_NAME,
TMI.OPERATOR_INSTANCE = OTMI.OPERATOR_INSTANCE, TMI.OPERATOR_INSTANCE2 = OTMI.OPERATOR_INSTANCE2,
TMI.OPERATOR_INSTANCE3 = OTMI.OPERATOR_INSTANCE3, TMI.SHIFT_NAME = OTMI.SHIFT_NAME,
TMI.INTERVAL_CATEGORY = OTMI.INTERVAL_CATEGORY, TMI.NTP_CATEGORY_NAME = OTMI.NTP_CATEGORY_NAME,
TMI.MACHINE_MODE = OTMI.MACHINE_MODE, TMI.JOB_LOAD_STATE_NAME = OTMI.JOB_LOAD_STATE_NAME,
TMI.RAW_SOURCE_MSG_TYPE = OTMI.RAW_SOURCE_MSG_TYPE
WHEN NOT MATCHED THEN
INSERT (TMI.MACHINE_INTERVAL_ID, TMI.START_DATE_TIME, TMI.INTERVAL_DURATION, TMI.CALC_END_TIME,
TMI.MACHINE_NAME, TMI.SITE_NAME, TMI.OPERATOR_INSTANCE, TMI.OPERATOR_INSTANCE2,
TMI.OPERATOR_INSTANCE3, TMI.SHIFT_NAME, TMI.INTERVAL_CATEGORY, TMI.NTP_CATEGORY_NAME,
TMI.MACHINE_MODE, TMI.JOB_LOAD_STATE_NAME, TMI.RAW_SOURCE_MSG_TYPE )
VALUES (MACHINE_INTERVAL_ID, START_DATE_TIME, INTERVAL_DURATION, CALC_END_TIME,
MACHINE_NAME, SITE_NAME, OPERATOR_INSTANCE, OPERATOR_INSTANCE2, OPERATOR_INSTANCE3,
SHIFT_NAME, INTERVAL_CATEGORY, NTP_CATEGORY_NAME, MACHINE_MODE,
JOB_LOAD_STATE_NAME, RAW_SOURCE_MSG_TYPE);
below is the full version of my newly modified code:
-- Denormaliztion of machine_interval Table.
-- Is used to take all intervals from interval_table and convert it from
-- foreign keys to corresponding names.
DECLARE
START_DATE_TIME TIMESTAMP(6) WITH TIME ZONE;
CALC_END_TIME TIMESTAMP(6) WITH TIME ZONE;
MACHINE_NAME VARCHAR2(256);
SITE_NAME VARCHAR2(256);
OPERATOR_INSTANCE VARCHAR2(256);
OPERATOR_INSTANCE2 VARCHAR2(256);
OPERATOR_INSTANCE3 VARCHAR2(256);
SHIFT_NAME VARCHAR2(256);
INTERVAL_CATEGORY VARCHAR2(256);
NPT_CATEGORY_NAME VARCHAR2(256);
MACHINE_MODE VARCHAR2(256);
JOB_LOAD_STATE_NAME VARCHAR2(256);
RAW_SOURCE_MSG_TYPE VARCHAR2(256);
INTERVAL_DURATION NUMBER;
MACHINE_INTERVAL_ID NUMBER;
--step one: Get all the intervals and store them into a cursor
CURSOR INTERVAL_CUR IS
SELECT *
FROM MACHINE_INTERVAL
ORDER BY START_DATE_TIME ASC;
TYPE TOTAL_MACHINE_INTERVALS IS
TABLE OF interval_cur%rowtype
INDEX BY PLS_INTEGER;
MACHINE_INTERVAL_ROW TOTAL_MACHINE_INTERVALS;
BEGIN
--step two: Make sure Test_Machine_interval is empty.
DELETE FROM TEST_MACHINE_INTERVAL;
OPEN INTERVAL_CUR;
LOOP
FETCH INTERVAL_CUR BULK COLLECT INTO MACHINE_INTERVAL_ROW LIMIT 100;
--step three: Loop through all the intervals.
FOR INDX IN 1..MACHINE_INTERVAL_ROW.COUNT
LOOP
--step four: Gather all datavalues needed to populate test_machine_interval.
MACHINE_INTERVAL_ID := MACHINE_INTERVAL_ROW(indx).MACHINE_INTERVAL_ID;
START_DATE_TIME := MACHINE_INTERVAL_ROW(indx).START_DATE_TIME;
CALC_END_TIME := MACHINE_INTERVAL_ROW(indx).CALC_END_TIME;
INTERVAL_DURATION := MACHINE_INTERVAL_ROW(indx).INTERVAL_DURATION;
INTERVAL_CATEGORY := MACHINE_INTERVAL_ROW(indx).INTERVAL_CATEGORY;
RAW_SOURCE_MSG_TYPE := MACHINE_INTERVAL_ROW(indx).RAW_SOURCE_MSG_TYPE;
SELECT M.MACHINE_NAME INTO MACHINE_NAME
FROM MACHINE M
WHERE MACHINE_ID = MACHINE_INTERVAL_ROW(indx).MACHINE_ID;
SELECT S.SITE_NAME INTO SITE_NAME
FROM SITE S
LEFT OUTER JOIN MACHINE M ON M.SITE_ID = S.SITE_ID
WHERE M.MACHINE_ID = MACHINE_INTERVAL_ROW(indx).MACHINE_ID;
SELECT O.OPERATOR_NAME INTO OPERATOR_INSTANCE
FROM OPERATOR_INSTANCE OI
LEFT OUTER JOIN OPERATOR O ON OI.OPERATOR_ID = O.OPERATOR_ID
WHERE OI.OPERATOR_INSTANCE_ID = MACHINE_INTERVAL_ROW(indx).OPERATOR_INSTANCE_ID;
SELECT O.OPERATOR_NAME INTO OPERATOR_INSTANCE2
FROM OPERATOR_INSTANCE OI
LEFT OUTER JOIN OPERATOR O ON OI.OPERATOR_ID = O.OPERATOR_ID
WHERE OI.OPERATOR_INSTANCE_ID = MACHINE_INTERVAL_ROW(indx).OPERATOR_INSTANCE_ID_2;
SELECT O.OPERATOR_NAME INTO OPERATOR_INSTANCE3
FROM OPERATOR_INSTANCE OI
LEFT OUTER JOIN OPERATOR O ON OI.OPERATOR_ID = O.OPERATOR_ID
WHERE OI.OPERATOR_INSTANCE_ID = MACHINE_INTERVAL_ROW(indx).OPERATOR_INSTANCE_ID_3;
SELECT NPT_CATEGORY_NAME INTO NPT_CATEGORY_NAME
FROM NPT_CATEGORY
WHERE NPT_CATEGORY_ID = MACHINE_INTERVAL_ROW(indx).NPT_CATEGORY_ID;
SELECT S.SHIFT_NAME INTO SHIFT_NAME
FROM SHIFTS S
LEFT OUTER JOIN SHIFT_TBL STBL ON S.SHIFT_ID = STBL.SHIFT_NAME_FK
WHERE STBL.SHIFT_ID_PK = MACHINE_INTERVAL_ROW(indx).SHIFT_ID;
SELECT MACHINE_MODE_NAME INTO MACHINE_MODE
FROM MACHINE_MODE MM
WHERE MM.MACHINE_MODE_ID = MACHINE_INTERVAL_ROW(indx).MACHINE_MODE_ID;
SELECT JLS.JOB_LOAD_STATE_NAME INTO JOB_LOAD_STATE_NAME
FROM JOB_LOAD_STATE JLS
WHERE JLS.JOB_LOAD_STATE_ID = MACHINE_INTERVAL_ROW(indx).JOB_LOAD_STATE_ID;
--step five: merge record into test_machine_interval.
MERGE INTO TEST_MACHINE_INTERVAL TMI
USING (SELECT * FROM TEST_MACHINE_INTERVAL) OTMI
ON (TMI.MACHINE_INTERVAL_ID = OTMI.MACHINE_INTERVAL_ID)
WHEN MATCHED THEN
UPDATE SET TMI.MACHINE_INTERVAL_ID = OTMI.MACHINE_INTERVAL_ID, TMI.START_DATE_TIME = OTMI.START_DATE_TIME,
TMI.INTERVAL_DURATION = OTMI.INTERVAL_DURATION, TMI.CALC_END_TIME = OTMI.CALC_END_TIME,
TMI.MACHINE_NAME = OTMI.MACHINE_NAME, TMI.SITE_NAME = OTMI.SITE_NAME,
TMI.OPERATOR_INSTANCE = OTMI.OPERATOR_INSTANCE, TMI.OPERATOR_INSTANCE2 = OTMI.OPERATOR_INSTANCE2,
TMI.OPERATOR_INSTANCE3 = OTMI.OPERATOR_INSTANCE3, TMI.SHIFT_NAME = OTMI.SHIFT_NAME,
TMI.INTERVAL_CATEGORY = OTMI.INTERVAL_CATEGORY, TMI.NTP_CATEGORY_NAME = OTMI.NTP_CATEGORY_NAME,
TMI.MACHINE_MODE = OTMI.MACHINE_MODE, TMI.JOB_LOAD_STATE_NAME = OTMI.JOB_LOAD_STATE_NAME,
TMI.RAW_SOURCE_MSG_TYPE = OTMI.RAW_SOURCE_MSG_TYPE
WHEN NOT MATCHED THEN
INSERT (TMI.MACHINE_INTERVAL_ID, TMI.START_DATE_TIME, TMI.INTERVAL_DURATION, TMI.CALC_END_TIME,
TMI.MACHINE_NAME, TMI.SITE_NAME, TMI.OPERATOR_INSTANCE, TMI.OPERATOR_INSTANCE2,
TMI.OPERATOR_INSTANCE3, TMI.SHIFT_NAME, TMI.INTERVAL_CATEGORY, TMI.NTP_CATEGORY_NAME,
TMI.MACHINE_MODE, TMI.JOB_LOAD_STATE_NAME, TMI.RAW_SOURCE_MSG_TYPE )
VALUES (MACHINE_INTERVAL_ID, START_DATE_TIME, INTERVAL_DURATION, CALC_END_TIME,
MACHINE_NAME, SITE_NAME, OPERATOR_INSTANCE, OPERATOR_INSTANCE2, OPERATOR_INSTANCE3,
SHIFT_NAME, INTERVAL_CATEGORY, NTP_CATEGORY_NAME, MACHINE_MODE,
JOB_LOAD_STATE_NAME, RAW_SOURCE_MSG_TYPE);
/*
EXECUTE IMMEDIATE 'INSERT INTO TEST_MACHINE_INTERVAL
(MACHINE_INTERVAL_ID, START_DATE_TIME, INTERVAL_DURATION, CALC_END_TIME,
MACHINE_NAME, SITE_NAME, OPERATOR_INSTANCE, OPERATOR_INSTANCE2,
OPERATOR_INSTANCE3, SHIFT_NAME, INTERVAL_CATEGORY, NTP_CATEGORY_NAME,
MACHINE_MODE,JOB_LOAD_STATE_NAME,RAW_SOURCE_MSG_TYPE )
VALUES(:1, :2, :3, :4, :5, :6, :7, :8, :9, :10, :11, :12, :13, :14, :15)'
USING MACHINE_INTERVAL_ID, START_DATE_TIME, INTERVAL_DURATION,
CALC_END_TIME, MACHINE_NAME, SITE_NAME,
OPERATOR_INSTANCE, OPERATOR_INSTANCE2, OPERATOR_INSTANCE3,
SHIFT_NAME, INTERVAL_CATEGORY, NTP_CATEGORY_NAME,
MACHINE_MODE,JOB_LOAD_STATE_NAME,RAW_SOURCE_MSG_TYPE;
*/
END LOOP;
EXIT WHEN MACHINE_INTERVAL_ROW.COUNT = 0;
END LOOP;
END;
I'm 75% sure that my problem lies in how i'm trying to fetch the bulk collection as displayed in the code above. So my question is: How exactly should i be fetching the value from a bulk collection to utilize with the merging of data?
And suggestions or comments are greatly appreciated. Thank you.
If you use FORALL, what needs to follow is a single SQL statement that you will pass the entire collection to. If you simply want to iterate over the elements in the collection, you'd use a FOR loop.
The syntax for referring to the n-th element of a collection is collection_name(index).column_name.
So, if you want to iterate over the elements in the collection one by one, you'd want something like
FOR indx IN MACHINE_INTERVAL_ROW.FIRST..MACHINE_INTERVAL_ROW.COUNT
LOOP
MACHINE_INTERVAL_ID := machine_interval_row(indx).MACHINE_INTERVAL_ID;
START_DATE_TIME := machine_interval_row(indx).START_DATE_TIME;
<<more code>>
END LOOP;
If you're going to refactor your code, though, I'm not sure what benefit you get from having a local variable MACHINE_INTERVAL_ID rather than just using machine_interval_row(indx).MACHINE_INTERVAL_ID. I'm also not sure why you're executing half a dozen separate SELECT statements each of which return a single row rather than writing one SELECT statement that joins together all these tables and populates whatever local variables you want.
Your MERGE is also going to be problematic-- it doesn't make sense for both the source and the destination of a MERGE to be the same table-- I would expect you to get an error that Oracle couldn't generate a stable set of rows if it tried to execute that statement. You could change the source of your query to be a query against DUAL that selected all the local variables you've populated, I guess, i.e.
MERGE INTO TEST_MACHINE_INTERVAL TMI
USING (SELECT machine_interval_row(indx).MACHINE_INTERVAL_ID,
machine_interval_row(indx).START_DATE_TIME
FROM dual) OTMI
ON (TMI.MACHIN_INTERVAL_ID = OTMI.MACHINE_INTERVAL_ID)
If TEST_MACHINE_INTERVAL is going to start off empty, though, it sounds like you'd be better off not using a MERGE, not using a BULK COLLECT and just writing an INSERT ... SELECT that pulled all the data you want to pull. Something like
INSERT INTO test_machine_interval( machine_interval_id,
start_date_time,
<<more columns>> )
SELECT machine_interval_id,
last_value(start_date_time) over (partition by machine_interval_id
order by start_date_time asc
rows between unbounded preceding
and unbounded following ) last_start_date_time,
<<more columns>>
FROM machine_interval