You could turn your input into a number (with maximum precision/scale), and then TO_CHAR that number twice - once with desired precision/scale as 9999D999, once with TM9 - and compare the two. If precision is broken, the first TO_CHAR returns ####; if scale is broken, the first TO_CHAR rounds - in either case the output will be different than the TM9 output.
So an example table:
create table emulate_external_table (
col varchar2(100)
);
insert into emulate_external_table (col) values ('123.456');
insert into emulate_external_table (col) values ('123.45');
insert into emulate_external_table (col) values ('9123.456');
insert into emulate_external_table (col) values ('123.4');
insert into emulate_external_table (col) values ('9123');
insert into emulate_external_table (col) values ('123');
commit;
In PL/SQL it'd be like this:
set serveroutput on size unlimited;
declare
l_will_fit boolean;
function will_fit (
p_string varchar2
, p_precision pls_integer
, p_scale pls_integer
)
return boolean
is
begin
return validate_conversion(p_string as number) = 1
and rtrim(to_char(to_number(p_string), 'FM' || rpad('9', p_precision, '9') || 'D' || rpad('9', p_scale, '9')), '.')
= to_char(to_number(p_string), 'TM9');
end;
begin
for eet in (
select col
from emulate_external_table
) loop
dbms_output.put(eet.col);
l_will_fit := will_fit(eet.col, 3, 2);
dbms_output.put(' ' || case when l_will_fit then 'fits' else 'do not fit' end || ' in number(3,2)');
l_will_fit := will_fit(eet.col, 4, 1);
dbms_output.put(' ' || case when l_will_fit then 'fits' else 'do not fit' end || ' in number(4,1)');
dbms_output.new_line;
end loop;
end;
/
123.456 do not fit in number(3,2) do not fit in number(4,1)
123.45 fits in number(3,2) do not fit in number(4,1)
9123.456 do not fit in number(3,2) do not fit in number(4,1)
123.4 fits in number(3,2) fits in number(4,1)
9123 do not fit in number(3,2) fits in number(4,1)
123 fits in number(3,2) fits in number(4,1)
RTRIM of '.' gets rid of trailing decimal point from 999D99 when the value is integer.
(This example assumes using decimal point, not decimal comma.)
Instead of pure PL/SQL, you could then also do pure SQL:
select
eet.col
, rtrim(to_char(to_number(eet.col), 'FM999D99'), '.') test_expr1
, to_char(to_number(eet.col), 'TM9') test_expr2
, case
when validate_conversion(eet.col as number) = 1
and rtrim(to_char(to_number(eet.col), 'FM999D99'), '.') = to_char(to_number(eet.col), 'TM9')
then 'Y'
else 'N'
end as will_fit_p3_s2
from emulate_external_table eet;
COL TEST_EXPR1 TEST_EXPR2 WILL_FIT_P3_S2
---------- ---------- ---------- --------------
123.456 123.46 123.456 N
123.45 123.45 123.45 Y
9123.456 ####### 9123.456 N
123.4 123.4 123.4 Y
9123 ####### 9123 N
123 123 123 Y
That way you can insert only those who fits:
insert into real_table
select
to_number(eet.col)
from emulate_external_table eet
where
case
when validate_conversion(eet.col as number) = 1
and rtrim(to_char(to_number(eet.col), 'FM999D99'), '.') = to_char(to_number(eet.col), 'TM9')
then 'Y'
else 'N'
end = 'Y'
;
But then again, if you load it like that, you could also use DML Error Logging instead, simply insert all rows from the external table using LOG ERRORS, and then all those that didn't fit the precision/scale would go to the error log table. That way you wouldn't have to code a check at all ;-)