I am getting data in Refcursor and binding in HTML table using for loop to send an email. When I am executive my procedure then it is giving me error of "numeric or value error: character string buffer too small". I have tried CLOB but still not get any success.
Error :-
ORA-06502: PL/SQL: numeric or value error: character string buffer too small
ORA-06512: at "XXCOMM.XX_EMS_MAIL_PRC_MAINTENANCE", line 113
ORA-06512: at line 6
Below id my procedure in which I am trying to send email.
create or replace PROCEDURE XX_EMS_MAIL_PRC_maintenance
(
p_site_id IN VARCHAR2
)
AS
l_in_date VARCHAR2(200) := NULL;
CURSOR cur_maintenance_list (p_site_id VARCHAR2)
IS
SELECT
c.instrument_no "Instrument_Number",
d.instrument_name "Instrument_Name",
d.eqp_srl_no "EQP_Serial_Number",
d.ownership "Ownership",
d.mfg_name "Mfg_Name",
d.model_no "Model_Number",
b.plan "Plan",
a.detail "Plan_Detail",
c.activity_id "Activity_Id",
c.maintenance_date "Maintenance_Date",
c.maintenance_due_date "Maintenance_Due_Date"
FROM
activity_detail a,
activity_master b,
maintenance_schedule c,
instrument_master d,
emp_master m
WHERE
a.plan_id = b.plan_id
AND d.instrument_no = c.instrument_no
AND c.plan_id = b.plan_id
AND c.activity_id = a.activity_id
AND ( c.instrument_no,
c.plan_id,
c.activity_id,
c.maintenance_due_date ) = (
SELECT
e.instrument_no,
e.plan_id,
e.activity_id,
MAX(e.maintenance_due_date)
FROM
maintenance_schedule e
WHERE
c.plan_id = e.plan_id
AND c.instrument_no = e.instrument_no
AND c.activity_id = e.activity_id
GROUP BY
e.instrument_no,
e.plan_id,
e.activity_id
)
AND trunc(c.maintenance_due_date) BETWEEN to_date('01-JUL-2022', 'DD-MON-RRRR') AND to_date('01-JUL-2022', 'DD-MON-RRRR')
AND d.p_manitenance_req <> 'N'
AND ( m.emp_initial = d.prepared_by
OR to_char(m.emp_no) = d.prepared_by )
AND ( b.site_id = d.site_id
OR d.site_id = '001' );
p_to sys_refcursor;
p_cc sys_refcursor;
p_bcc sys_refcursor;
v_html_msg VARCHAR2(32672) := NULL;
--v_txt_msg varchar2(250) := 'WelCome ....'||chr(10)||'this is a test mail';
v_to VARCHAR2 (1000) := NULL;
v_cc VARCHAR2 (1000) := NULL;
v_bcc VARCHAR2 (2000) := NULL;
v_from VARCHAR2 (150) := 'abc.com';
v_db_name varchar2(25) := null;
BEGIN
BEGIN
v_db_name := null;
SELECT listagg(EMAIL, ',') WITHIN GROUP (order by Email) INTO v_to FROM sml.xx_lsp_email_master WHERE email_type = 'TO' AND function_name = 'EMS_App_maintenance' AND isactive='Y';
SELECT listagg(EMAIL, ',') WITHIN GROUP (order by Email) INTO v_cc FROM sml.xx_lsp_email_master WHERE email_type = 'CC' AND function_name = 'EMS_App_maintenance' AND isactive='Y';
--SELECT listagg(EMAIL, ',') WITHIN GROUP (order by Email) INTO v_bcc FROM sml.xx_lsp_email_master WHERE email_type = 'BCC' AND function_name = 'EMS_App';
v_html_msg :='<html><head></head><body><p>Dear All,<br/><br/>
Please find the details of Maintenance due list for this week in Equipment Management System</p><br/>
<table border=1>
<tr>
<th> I/S Number </th>
<th> Instrument Name </th>
<th> EQP Serial Number </th>
<th> Ownership </th>
<th> Make </th>
<th> Model Number </th>
<th> Plan </th>
<th> Plan Detail </th>
<th> Activity Id </th>
<th> Maintenance Date </th>
<th> Maintenance Due Date </th>
</tr>';
FOR i IN cur_maintenance_list (p_site_id)
LOOP
--SELECT (trunc(to_date(i."maintenance_date", 'DD-MON-RRRR') - 15)) into l_in_date from dua
v_html_msg :=
v_html_msg
|| '<tr align="left"><td>'
|| i."Instrument_Number"
|| '</td><td>'
|| i."Instrument_Name"
|| '</td><td>'
|| i."EQP_Serial_Number"
|| '</td><td>'
|| i."Ownership"
|| '</td><td>'
|| i."Mfg_Name"
|| '</td><td>'
|| i."Model_Number"
|| '</td><td>'
|| i."Plan"
|| '</td><td>'
|| i."Plan_Detail"
|| '</td><td>'
|| i."Activity_Id"
|| '</td><td>'
|| i."Maintenance_Date"
|| '</td><td>'
|| i."Maintenance_Due_Date"
|| '</td>
</Tr>';
END LOOP;
v_html_msg :=
v_html_msg
|| '</table>
<p>This is a system generated mail. Please do not reply on this mail.
<br/>
<br/>
Regards,
<br/>
EQP.
</p>
</body>
</html>';
send_mail_recipient(p_to => v_to,
p_cc => v_cc,
p_bcc => v_bcc,
p_from => v_from,
p_subject => 'Mail notification about Maintenance Due for this week Equipment Management System',
--p_text_msg => v_txt_msg,
p_html_msg => v_html_msg,
p_smtp_host => 'smtprelay.com');
END;
null;
END XX_EMS_MAIL_PRC_maintenance;
Line 113 is
v_html_msg :=
v_html_msg
|| '<tr align="left"><td>'
...
That variable is declared as
v_html_msg VARCHAR2(32672)
which means that - as you're processing data in a loop - its length exceeds 32672 characters in length.
What to do? Use CLOB instead, or check whether there's really that much data (i.e. maybe cursor returned more rows than you'd expect - check its SELECT statement).
The main problem is that v_html_msg is defined as VARCHAR2(32672) and the loop is trying to add more than 32672 characters there.
The full solution is to change v_html_msg datatype to CLOB. However, it will require you to refactor this procedure XX_EMS_MAIL_PRC_maintenance and other dependent one, like send_mail_recipient.
The workaround may be to send mail with only partial data (of course if it is OK with your business requirements). You can add an "if" in the loop so once the length of v_html_msg is larger than 32000 - stop adding more items there. For example:
FOR i IN cur_maintenance_list (p_site_id)
LOOP
if length(v_html_msg) <30000 then --<<< NEW CODE
v_html_msg :=
v_html_msg
|| '<tr align="left"><td>'
|| i."Instrument_Number"
|| '</td><td>'
|| i."Instrument_Name"
|| '</td><td>'
|| i."EQP_Serial_Number"
|| '</td><td>'
|| i."Ownership"
|| '</td><td>'
|| i."Mfg_Name"
|| '</td><td>'
|| i."Model_Number"
|| '</td><td>'
|| i."Plan"
|| '</td><td>'
|| i."Plan_Detail"
|| '</td><td>'
|| i."Activity_Id"
|| '</td><td>'
|| i."Maintenance_Date"
|| '</td><td>'
|| i."Maintenance_Due_Date"
|| '</td>
</Tr>';
else --<<< NEW CODE
v_html_msg :=
v_html_msg ||'... and more rows exist...';
end if;
END LOOP;
Related
I have a view (DB_ADMIN.VW_DBA_MONITOR_CURRENTLYEXEC) to be able to visualize the transactions in Oracle. This view makes use of those already existing in the system: v$sql, V$SQLSTATS, v$sqlarea and gv$process.
And the structure and type of columns was as follows:
I have a procedure that attempts to use such a view, format the output to a single HTML string under certain conditions, and send it via email. Here is a simple summary of that procedure:
DECLARE
V_BODY_TEXT CLOB;
BEGIN
select utl_i18n.unescape_reference(xmlagg(xmlelement(e,'
<tr>
<td>' || ltrim(rtrim(TO_CLOB(SID))) || '</th>
<td>' || ltrim(rtrim(TO_CLOB(SQL_ID))) || '</th>
<td>' || ltrim(rtrim(TO_CLOB(USERNAME))) || '</th>
<td>' || ltrim(rtrim(TO_CLOB(ONAME))) || '</th>
<td>' || ltrim(rtrim(TO_CLOB(MACHINE))) || '</th>
<td>' || ltrim(rtrim(TO_CLOB(WAIT_CLASS))) || '</th>
<td>' || ltrim(rtrim(TO_CLOB(EVENT))) || '</th>
<td>' || ltrim(rtrim(TO_CLOB(MODULE))) || '</th>
<td>' || ltrim(rtrim(TO_CLOB(LOGON_TIME))) || '</th>
<td>' || ltrim(rtrim(TO_CLOB(START_TIME))) || '</th>
<td>' || ltrim(rtrim(TO_CLOB(TIME))) || '</th>
<td>' || ltrim(rtrim(TO_CLOB(SQL_TEXT))) || '</th>
</tr>
','').extract('//text()')).getclobval()) x
INTO V_BODY_TEXT
FROM DB_ADMIN.VW_DBA_MONITOR_CURRENTLYEXEC WHERE "TIME" > 300;
END;
The error presented by the execution of the above code is because the concatenated result only admits 4000 characters and it doesn't matter if I try to cast (TO_CLOB) field by field or the entire concatenated string, I still get the same error every time. result size is exceeded:
About the above code, the use of the TO_CLOB function, is my last attempt to try to solve the problem.
Said error does not allow any "substring" or "len" type action to be applied to it either.
I've also tried creating a table with all the fields of type CLOB, then inserting the result of the view, and using the above procedure now with the table, but I keep getting the same error every time the generated html code exceeds 4000 characters.
Therefore, I go to this community to see what other options they give me to get out of the problem.
The xmlelement function only accepts up to 4000 characters if the initialization parameter MAX_STRING_SIZE = STANDARD, and 32767 characters if MAX_STRING_SIZE = EXTENDED, so there's no point in converting to CLOB that early in your process. See here.
Also, the utl_i18n.unescape_reference function only accepts VARCHAR2 input, not CLOB. See here. This is the most likely place your query is failing.
Try this, working exclusively with VARCHAR2, doing explicit conversions from NUMBER and DATE types, and saving the aggregation into the largest string to the very end:
DECLARE
V_BODY_TEXT CLOB;
BEGIN
select xmlagg(utl_i18n.unescape_reference(xmlelement(e,'
<tr>
<td>' || TO_CHAR(SID) || '</th>
<td>' || ltrim(rtrim(SQL_ID)) || '</th>
<td>' || ltrim(rtrim(USERNAME)) || '</th>
<td>' || ltrim(rtrim(ONAME)) || '</th>
<td>' || ltrim(rtrim(MACHINE)) || '</th>
<td>' || ltrim(rtrim(WAIT_CLASS)) || '</th>
<td>' || ltrim(rtrim(EVENT)) || '</th>
<td>' || ltrim(rtrim(MODULE)) || '</th>
<td>' || TO_CHAR(LOGON_TIME,'DD-MON-YYYY') || '</th>
<td>' || TO_CHAR(START_TIME,'DD-MON-YYYY') || '</th>
<td>' || TO_CHAR(TIME) || '</th>
<td>' || ltrim(rtrim(TO_CLOB(SQL_TEXT))) || '</th>
</tr>
',''))) x
INTO V_BODY_TEXT
FROM DB_ADMIN.VW_DBA_MONITOR_CURRENTLYEXEC WHERE "TIME" > 300;
END;
XMLType methods are deprecated since Oracle 11gr2, use modern (though, it's hard to tell about XML) SQL functions to access XML data without additional serialization/deserialization issues:
XMLQUERY for extraction.
XMLCAST for deserialization. It does decoding of HTML/XML stuff like & etc to their text values.
Below is an example of three characters padded with zeroes up to 5000 bytes (so resulting in too large value for varchar2)
with t(col) as (
select
xmlelement(e,
to_clob(rpad('<>&', 4000, '0'))
|| to_clob(rpad('0', 1000, '0'))
)
from dual
)
, deser as (
select
xmlcast(xmlquery(
'//text()'
passing col
returning content
) as clob) as res
from t
)
select
length(res),
trim(trailing '0' from res) as res
from deser
LENGTH(RES)
RES
5000
<>&
fiddle
However, you may avoid concatenation at all and serialize SQL result directly into XML with dbms_xmlgen package. Below is an example:
with function f_get_html(
p_rows int
)
return clob
as
l_ctx number;
l_cursor sys_refcursor;
l_res clob;
begin
/* Cursor object is used to be able to use parameters for statement.
In case of static statement may be replaced by string literal
containing SQL statement directly in newcontext call
*/
open l_cursor for
select
dummy as "td", level as "td",
'<>&' as "td"
from dual
connect by level < p_rows
;
/*Use context to set row tags to <tr>*/
l_ctx := dbms_xmlgen.newcontext(
l_cursor
);
dbms_xmlgen.setrowtag(l_ctx, 'tr');
/*Extract content as HTML-formatted clob*/
select xmlserialize(content xmlquery(
'/ROWSET/*'
passing dbms_xmlgen.getxmltype(l_ctx)
returning content
) as clob)
into l_res
from dual;
return l_res;
end;
select f_get_html(4) as res_html
from dual
RES_HTML
<tr><td>X</td><td>1</td><td><>&</td></tr><tr><td>X</td><td>2</td><td><>&</td></tr><tr><td>X</td><td>3</td><td><>&</td></tr>
fiddle
Finally, the solution I found to avoid the various restrictions of the functions used to generate xml code or converters like the utl_i18n.unescape_reference function, was to use a loop with FOR:
DECLARE
V_BODY CLOB;
V_TABLE_BODY CLOB := NULL;
BEGIN
--> DataSource:
FOR r IN (
SELECT SID,
SQL_ID,
USERNAME,
ONAME,
MACHINE,
WAIT_CLASS,
EVENT,
MODULE,
LOGON_TIME,
START_TIME,
TIME,
SQL_TEXT
FROM DB_ADMIN.VW_DBA_MONITOR_CURRENTLYEXEC
WHERE "TIME" > 300
)
LOOP
--> Table Body:
V_TABLE_BODY := V_TABLE_BODY ||
'<tr>'||
'<td>'||trim(r.SID)||'</td>' ||
'<td>'||trim(r.SQL_ID)||'</td>' ||
'<td>'||trim(r.USERNAME)||'</td>' ||
'<td>'||trim(r.ONAME)||'</td>' ||
'<td>'||trim(r.MACHINE)||'</td>' ||
'<td>'||trim(r.WAIT_CLASS)||'</td>' ||
'<td>'||trim(r.EVENT)||'</td>' ||
'<td>'||trim(r.MODULE)||'</td>' ||
'<td>'||trim(r.LOGON_TIME)||'</td>' ||
'<td>'||trim(r.START_TIME)||'</td>' ||
'<td>'||trim(r.TIME)||'</td>' ||
'<td>'||trim(r.SQL_TEXT)||'</td>' ||
'</tr>';
END LOOP;
----
IF V_TABLE_BODY IS NOT NULL THEN
--> HTML Body:
V_BODY :=
'
<body>
<table border="1">
<tr>
<th>SID</th>
<th>SQL_ID</th>
<th>USERNAME</th>
<th>ONAME</th>
<th>MACHINE</th>
<th>WAIT_CLASS</th>
<th>EVENT</th>
<th>MODULE</th>
<th>LOGON_TIME</th>
<th>START_TIME</th>
<th>TIME</th>
<th>SQL_TEXT</th>
</tr>' ||
V_TABLE_BODY || --> Table Body
'</table>
</body>
';
-- Send Mail:
DB_ADMIN.USP_SEND_MAIL(V_BODY); --> Referential
END IF;
Do any database engines have the concept of a C-like macro? Here would be an example where I'd just like to make it more readable:
SELECT
SUM(Profit) AS Total,
(SELECT AVG(Profit) FROM This
WHERE Category=This.Category AND Product=This.Product
AND PARSE_DATE('%M %d%, Y', CONCAT(This.Month ' 1, ' This.Year))
BETWEEN PARSE_DATE('%M %d%, Y', CONCAT(This.Month ' 1, ' This.Year))
AND PARSE_DATE('%M %d%, Y', CONCAT(This.Month ' 1, ' This.Year))-INTERVAL 3 MONTH
FROM Tbl
I would rather have something that looks like:
#define dt PARSE_DATE('%M %d%, Y', CONCAT(This.Month ' 1, ' This.Year))
SELECT
SUM(Profit) AS Total,
(SELECT AVG(Profit) FROM This
WHERE Category=This.Category AND Product=This.Product
AND dt BETWEEN dt AND (dt-INTERVAL 3 MONTH)
FROM Tbl
Does something like that exist or commonly-used in the major DBMSs?
From Oracle 12, you can declare a function inside a sub-query factoring (WITH) clause:
WITH FUNCTION dt (month INT, year INT) RETURN DATE AS
BEGIN
RETURN TO_DATE(year || '-' || month || '-01', 'YYYY-MM-DD');
END;
SELECT *
FROM this
WHERE dt(this.month, this.year)
BETWEEN ADD_MONTHS(dt(this.month, this.year), -3)
AND dt(this.month, this.year);
db<>fiddle here
From Oracle 21, you can write SQL macros:
CREATE FUNCTION dt (month INT, year INT)
RETURN VARCHAR2 SQL_MACRO(SCALAR)
AS
BEGIN
RETURN 'TO_DATE(year || ''-'' || month || ''-01'', ''YYYY-MM-DD'')';
END;
/
Then would use it as:
SELECT *
FROM this
WHERE dt(this.month, this.year)
BETWEEN ADD_MONTHS(dt(this.month, this.year), -3)
AND dt(this.month, this.year);
And the query would get rewritten as:
SELECT *
FROM this
WHERE TO_DATE(this.year || '-' || this.month || '-01', 'YYYY-MM-DD')
BETWEEN ADD_MONTHS(TO_DATE(this.year || '-' || this.month || '-01', 'YYYY-MM-DD'), -3)
AND TO_DATE(this.year || '-' || this.month || '-01', 'YYYY-MM-DD');
1.What DBMS_OUTPUT.PUT_LINE(‘God is' || NULL || NULL
|| ‘Great' ) displays?
2.Is it valid?
DECLARE i, j, k NUMBER(2);
1.What DBMS_OUTPUT.PUT_LINE(‘God is' || NULL || NULL || ‘Great' ) displays?
o/p God is great
Is it valid? DECLARE i, j, k NUMBER(2);
No, this is statement is not valid, we should assign all these variables separately eg.
I NUMBER(2);
j NUMBER(2);
k NUMBER(2);
How can I convert following Oracle line to MYSQL?
cur_var rout_acc_cursor%ROWTYPE;
type of rout_acc_cursor is CURSOR
these cur_var and rout_acc_cursor is used as follows
OPEN rout_acc_cursor;
FETCH rout_acc_cursor
INTO cur_var;
WHILE rout_acc_cursor%FOUND
LOOP
routing_ac := cur_var.routing_ac;
-- sharicomplient := cur_var.u06_sharia_complient;
sexchange := cur_var.u06_exchange;
comm_cat := cur_var.u06_commision_type;
comm_group_id := cur_var.u06_commision_group_id;
parms :=
parms
|| '!'
|| cur_var.u06_exchange
|| ','
|| cur_var.u06_routing_ac_type
|| ','
|| NVL (routing_ac, '*')
|| ',';
Basically I'm looking for something like
SELECT ordinal(my_number) FROM my_table
which would return
1st
11th
1071st
...
etc
but preferrably without the use of a stored procedure
I don't know of a built-in function but it's pretty easy to write:
SELECT
CONCAT(my_number, CASE
WHEN my_number%100 BETWEEN 11 AND 13 THEN "th"
WHEN my_number%10 = 1 THEN "st"
WHEN my_number%10 = 2 THEN "nd"
WHEN my_number%10 = 3 THEN "rd"
ELSE "th"
END)
FROM my_table;
mysql doesn't have support for this. You'll have to handle the strings in whichever language you are getting the mysql data from.
Based on Ken's code, a custom MySQL function would be as follows:
DELIMITER $$
CREATE FUNCTION ordinal(number BIGINT)
RETURNS VARCHAR(64)
DETERMINISTIC
BEGIN
DECLARE ord VARCHAR(64);
SET ord = (SELECT CONCAT(number, CASE
WHEN number%100 BETWEEN 11 AND 13 THEN "th"
WHEN number%10 = 1 THEN "st"
WHEN number%10 = 2 THEN "nd"
WHEN number%10 = 3 THEN "rd"
ELSE "th"
END));
RETURN ord;
END$$
DELIMITER ;
Then it can be used as:
SELECT ordinal(1) -- 1st
SELECT ordinal(11) -- 11th
SELECT ordinal(21) -- 21st
SELECT ordinal(my_number) FROM my_table
It is possible in MySQL using the string functions but it gets messy real fast. You'd better just do the suffix in the language you're using. For example, in PHP you could do something like this:
function ordSuffix($num) {
if(empty($num) || !is_numeric($num) || $num == 0) return $num;
$lastNum = substr($num, -1);
$suffix = 'th';
if($lastNum == 1 && $num != 11) { $suffix = 'st'; }
elseif($lastNum == 2 && $num != 12) { $suffix = 'nd'; }
elseif($lastNum == 3 && $num != 13) { $suffix = 'rd'; }
return $num.$suffix;
}
echo ordSuffix(4); // 4th
echo ordSuffix(1); // 1st
echo ordSuffix(12); // 12th
echo ordSuffix(1052); // 1052nd
I found a way that works for me but its a bit of a hack
DATE_FORMAT(CONCAT('2010-01-', my_number), '%D')
That works because currently the number I'm looking at never gets above 25. But it doesn't generalize well so someone might be entertained by this:
CONCAT(
IF(my_number % 100 BETWEEN 11 AND 13,
FLOOR(my_number / 100),
FLOOR(my_number / 10)),
DATE_FORMAT(
CONCAT('2010-01-',
IF(my_number % 100 BETWEEN 11 AND 13
my_number % 100,
my_number % 10)),
'%D'))
But that's a lot of work just to get at the DATE_FORMAT functionality when Ken's code is simpler.