TRANSLATE not working inside XMLAGG - sql

I want to use TRANSLATE to replace invalid XML characters in a string that's used in XMLAGG instead of nested REPLACE due to the large number of characters that need to be replaced:
select c.key,
cast( substr( xmlserialize( xmlagg( xmltext( concat( ' | ',
replace(replace(replace(replace(c.comment_text,chr(160),' '), chr(191),' '), chr(176),' '), chr(190),' ')
--translate(c.comment_text, ' ', chr(160)||chr(176)||chr(190)||chr(191))
) ) order by c.comment_date desc ) as clob ), 3, 4000 ) as varchar2) as sum_comment
from comments c
where c.comment_date > current_timestamp - 90
group by c.key
If I run this outside of the XMLAGG I can see that both the TRANSLATE and REPLACE are removing the invalid characters, but inside the XMLAGG I get the error I'm trying to avoid when using TRANSLATE:
An illegal XML character "#xA0" was found in an SQL/XML expression or function argument that begins with string " | A".

Related

XMLAGG Throws Inconsistent Datatypes: Expected - got CLOB

In the first place, my query (actually subquery, continue on reading) contains a LISTAGG, but I need to replace it with XMLAGG when users are started to get result of string concatenation is too long errors.
So, here is my subquery. I'm not sharing the whole query, because first it is too long and second its not so relevant with what I'm asking. I'll provide the data types nevertheless.
(select rtrim((xmlagg(xmlelement(e, sa1.answer || ',') order by sa1.answer).EXTRACT('//text()')).getclobval(), ',')
from social_feed_answer sa1 where sa1.docid = f.id and sa1.issent = 1 and sa1.answertype in
('Reply', 'PrivateMessage', 'ApproveFeed','RejectFeed') and sa1.isactive = 1) as Answers,
Before your suggestions, I have already tried to remove RTRIM and ORDER BY clauses. None of them helps. I got different types of error e.g. Conversion of special character to escaped character failed or Character string buffer too small error.
My oracle version is 19c and that sa1.answer is nvarchar(2000).
I saw that this query works on other people flawless, but cannot figure out what's the case for me.
Thank you in advance.
One issue appears to be that sa1.answer || ',' raises an exception and not that there is a problem with XMLAGG. To fix that you can use EMPTY_CLOB() || sa1.answer || ',' or TO_CLOB(sa1.answer) || ',' so that the first value is a CLOB, and not a NVARCHAR2, and then the concatenated string will not be too large.
select rtrim(
xmlagg(
xmlelement(e, EMPTY_CLOB() || sa1.answer || ',')
order by sa1.answer
).EXTRACT('//text()').getclobval(),
','
)
from social_feed_answer sa1
-- ...
or:
select rtrim(
xmlagg(
xmlelement(e, TO_CLOB(sa1.answer) || ',')
order by sa1.answer
).EXTRACT('//text()').getclobval(),
','
)
from social_feed_answer sa1
-- ...
db<>fiddle here
(select xmlagg(xmlelement(e, REGEXP_REPLACE(sa1.answer, '[^[:print:]]', '') || ' , ')
order by sa1.answer).EXTRACT('//text()') from social_feed_answer sa1 where sa1.docid = f.id and sa1.issent = 1 and sa1.answertype in
('Reply', 'PrivateMessage', 'ApproveFeed', 'RejectFeed') and sa1.isactive = 1) as Answers
Above query works out without any issue.
REGEXP_REPLACE function is necessary only if your are facing ORA-64451: Conversion of special character to escaped character failed error. And, have to give up RTRIM function as this function converts the result type to VARCHAR2 which might makes you end up with result of string concatenation is too long error (by the way, getClobVal() right after XMLAGG didn't help).

why output is null at select translate(' #',' ','') from dual; and why resulit is # at select replace(' #',' ','') from dual;

Basically translate will change character to character and Replace string to string , and here i have tried to remove spaces using translate to count the number words .
select translate(' #',' ','') from dual;
select replace(' #',' ','') from dual;
select ename , nvl(length(replace(TRANSLATE(upper(trim(ename)),'ABCDEFGHIJKLMNOPQRSTUVWXYZ'' ',' # '),' ',''))+1,1) NOOFWORDs
from emp;
Unfortunately Oracle has made many bizarre choices around null vs. empty string.
One of those has to do with TRANSLATE. TRANSLATE will return NULL if any of its arguments (including the last one) is NULL, no matter what the logical behavior should be.
So, to remove spaces (say) with TRANSLATE, you must add a character you do NOT want to be removed to both the second and the third argument. I added the lower-case letter z, but you could add anything (a dot, the digit 0, whatever - just make sure you add the same character at the beginning of both arguments)
... translate (input_string, 'z ', 'z') ....
For example:
select translate(' #','z ','z') from dual;
TRANSLATE('#','Z','Z')
------------------------
#
select translate(' #',' ','') from dual;
Returns NULL because in Oracle empty strings unfortunately yield NULLs. Therefore it's equivalent to
SELECT translate(' #', ' ', NULL)
FROM dual;
and translate() returns NULL when an argument is null. Actually this is well documented in "TRANSLATE":
(...)
You cannot use an empty string for to_string to remove all characters in from_string from the return value. Oracle Database interprets the empty string as null, and if this function has a null argument, then it returns null.
If you want to replace one character, use replace() as you already did. For a few but more than one characters you can nest the replace()s.
This however gets unhandy, when you want to replace quite a lot of characters. In such a situation, if the replacement character is only one character or the empty string regexp_replace() using a character class or alternates may come in handy.
For example
SELECT regexp_replace('a12b478c01', '[0-9]', '')
FROM dual;
replaces all the digits so just 'abc' remains and
SELECT regexp_replace('ABcc1233', 'c|3', '')
FROM dual;
removes any '3' or 'c' and results in 'AB12'. In your very example
SELECT regexp_replace(' #', ' ', '')
FROM dual;
would also work and give you '#'. Though in the simple case of your example a simple replace() is enough.

How to remove specific value from comma separated string in oracle

I want remove specific value from comma separated sting using oracle.
Sample Input -
col
1,2,3,4,5
Suppose i want to remove 3 from the string.
Sample Output -
col
1,2,4,5
Please suggest how i can do this using oracle query.
Thanks.
Here is a solution that uses only standard string functions (rather than regular expressions) - which should result in faster execution in most cases; it removes 3 only when it is the first character followed by comma, the last character preceded by comma, or preceded and followed by comma, and it removes the comma that precedes it in the middle case and it removes the comma that follows it in the first and third case.
It is able to remove two 3's in a row (which some of the other solutions offered are not able to do) while leaving in place consecutive commas (which presumably stand in for NULL) and do not disturb numbers like 38 or 123.
The strategy is to first double up every comma (replace , with ,,) and append and prepend a comma (to the beginning and the end of the string). Then remove every occurrence of ,3,. From what is left, replace every ,, back with a single , and finally remove the leading and trailing ,.
with
test_data ( str ) as (
select '1,2,3,4,5' from dual union all
select '1,2,3,3,4,4,5' from dual union all
select '12,34,5' from dual union all
select '1,,,3,3,3,4' from dual
)
select str,
trim(both ',' from
replace( replace(',' || replace(str, ',', ',,') || ',', ',3,'), ',,', ',')
) as new_str
from test_data
;
STR NEW_STR
------------- ----------
1,2,3,4,5 1,2,4,5
1,2,3,3,4,4,5 1,2,4,4,5
12,34,5 12,34,5
1,,,3,3,3,4 1,,,4
4 rows selected.
Note As pointed out by MT0 (see Comments below), this will trim too much if the original string begins or ends with commas. To cover that case, instead of wrapping everything within trim(both ',' from ...) I should wrap the rest within a subquery, and use something like substr(new_str, 2, length(new_str) - 2) in the outer query.
Here is one method:
select trim(both ',' from replace(',' || '1,2,3,4,5' || ',', ',' || '3' || ',', ','))
That said, storing comma-delimited strings is a really, really bad idea. There is almost no reason to do such a thing. Oracle supports JSON, XML, and nested tables -- all of which are better alternatives.
The need to remove an element suggests a poor data design.
You can convert the list rows using an XMLTABLE, filter to remove the unwanted rows and then re-aggregate them:
SELECT LISTAGG( x.value.getStringVal(), ',' ) WITHIN GROUP ( ORDER BY idx )
FROM XMLTABLE(
( '1,2,3,4,5' )
COLUMNS value XMLTYPE PATH '.',
idx FOR ORDINALITY
) x
WHERE x.value.getStringVal() != 3;
For a simple filter this is probably not worth it and you should use something like (based on #mathguy's solution):
SELECT SUBSTR( new_list, 2, LENGTH( new_list ) - 2 ) AS new_list
FROM (
SELECT REPLACE(
REPLACE(
',' || REPLACE( :list, ',', ',,' ) || ',',
',' || :value_to_replace || ','
),
',,',
','
) AS new_list
FROM DUAL
)
However, if the filtering is more complicated then it might be worth converting the list to rows, filtering and re-aggregating.
I do not knwo how to do this in Oracle, but with SQL-Server I'd use a trick:
convert the list to XML by replacing the comma with tags
use XQuery to filter the data
reconcatenate
This is SQL Server syntax but might point you the direction:
declare #s varchar(100)='1,2,2,3,3,4';
declare #exclude int=3;
WITH Casted AS
(
SELECT CAST('<x>' + REPLACE(#s,',','</x><x>') + '</x>' AS XML) AS TheXml
)
SELECT x.value('.','int')
FROM Casted
CROSS APPLY TheXml.nodes('/x[text()!=sql:variable("#exclude")]') AS A(x)
UPDATE
I just found this answer which seems to show pretty well how to start...
I agree with Gordon regarding the fact that storing comma delimited data in a column is a really bad idea.
I just preceed the csv with a ',', then use the replace function followed by a left trim function to clean-up the preceeding ','.
SCOTT#tst>VAR b_number varchar2(5);
SCOTT#tst>EXEC :b_number:= '3';
PL/SQL procedure successfully completed.
SCOTT#tst>WITH srce AS (
2 SELECT
3 ',' || '3,1,2,3,3,4,5,3' col
4 FROM
5 dual
6 ) SELECT
7 ltrim(replace(col,',' ||:b_number),',') col
8 FROM
9 srce;
COL
1,2,4,5

Selecting individual values from csv format in oracle pl sql

I have the following value in a column in Oracle db ('abc', 'xyz')
I want to extract the values separately like abc, xyz by removing ' and (). Is there a way to do it using INSTR and SUBSTR functions?
Thanks
Use this query:
with sample as (select '(''abc'', ''xyz'')' text from dual)
select substr(text,instr(text,'''',1,1) + 1,instr(text,'''',1,2) - instr(text,'''',1,1) - 1),
substr(text,instr(text,'''',1,3) + 1,instr(text,'''',1,4) - instr(text,'''',1,3) - 1)
from sample;
It would help to know what you want to do with the data once parsed. How it could be handled in SQL vs PL/SQL to achieve your requirement could be very different.
That said, here's one way to strip surrounding parens and remove single quotes at the same time during the select using the powerful regexp_replace(source_string, pattern_string, replace_string) :
WITH qry AS (SELECT '(' || '''abc''' || ',' || '''xyz''' || ')' orig_string
FROM dual
)
SELECT regexp_replace(orig_string, '[()'']', '' ) clean_string
FROM qry;
The regexp_replace pattern_string says to match a character class (defind by opening and closing square brackets) containing a left paren or a right paren or a single quote (quoted so Oracle sees it) and the replace_string replaces it with nothing.
Then, to parse the values remaining here's an example from by bag of tricks I got somewhere and tweaked for this case:
set serveroutput on
DECLARE
-- Build a string in the format "('abc','xyz')"
orig_string varchar2(20) := '(' || '''abc''' || ',' || '''xyz''' || ')';
CURSOR cur IS
WITH qry AS (SELECT regexp_replace(orig_string, '[()'']','' ) clean_string
FROM dual
)
SELECT regexp_substr(clean_string, '[^,]+', 1, ROWNUM) element
FROM qry
CONNECT BY LEVEL <= LENGTH(regexp_replace (clean_string, '[^,]+')) + 1;
BEGIN
FOR rec IN cur LOOP
dbms_output.put_line('Element:' || rec.element);
END LOOP;
END;
It basically loops through the elements and prints them. I'm sure you can adapt this to your situation.

find comma in oracle sql string

I have a column in my oracle db as character and data stored in this is like here
30.170527093355002,72.615875338654 and
30.805165,71.82474
Now I want to get the separated by comma whole string. I mean I want to get the part of string before comma and also part after comma separately. Please any one tell me is there any built in function to do this that I can separate my while string by comma regardless of comma position where it exist.I have already tried floor function and substr but all in vain please help me to use any built in function or user defined function to full fill my requirements.
select
substr( COLNAME, 1, instr( COLNAME, ',') - 1 ) as p_1 ,
substr( COLNAME, instr( COLNAME, ',', - 1 ) + 1 ) as p_2
from YOURTABLE