How to identify invalid (corrupted) values stored in Oracle DATE columns - sql

Oracle 10.2.0.5
What is the easiest way to identify rows in a table that have "invalid" values in DATE columns. By "invalid" here what I mean is a binary representation that violates Oracle rules for date values.
I recently had an issue with an invalid date stored in a column.
I was able to use a query predicate to find a particular problematic row:
WHERE TO_CHAR(date_expr,'YYYYMMDDHH24MISS') = '00000000000000'
In the case I had, the century byte was invalid...
select dump(h.bid_close_date) from mytable h where h.id = 54321
Typ=12 Len=7: 220,111,11,2,1,1,1
The century byte should be 100 + two digit century. In this case, there was an extra 100 added, as if the century value was "120", making the year "12011". (The only way I know to get invalid DATE values into the database is using OCI, using native 7-byte DATE representation.)
In this case, the TO_CHAR function returned an identifiable string, which I could use for identifying the wonky DATE value.
My question: is there an more general or easier approach (preferably using a SQL SELECT statement) to identify rows with "invalid" values in DATE columns.

This is a pretty unusual scenario (although I have come across something similar once before). The more common problem is finding invalid dates which are held as strings in a date column. You could adapt the solution for that to your situation, by building your own date validator.
Something like this:
create or replace function is_a_date
( p_date in date )
return varchar2
is
d date;
begin
d := to_date(to_char(p_date, 'SYYYYMMDDHH24MISS'), 'SYYYYMMDDHH24MISS') ;
if d != p_date then
return 'not a proper date';
else
return 'good date';
end if;
exception
when others then
return 'not a date';
end;
/
This converts a date into a string and back again. It catches exceptions thrown by date casting. If the end product is not the same as the input date then presumably something got lost in translation; to be honest I'm not sure whether the 12011 date would cast successfully to a string, so this is a belt'n'braces approach. It's a bit tricky writing this utility without some test data!
This query would identify all the non-valid dates:
select h.id, dump(h.bid_close_date)
from mytable h
where h.bid_close_date is not null
and is_a_date(h.bid_close_date) != 'good date';

Without adding a function, a simple predicate
TO_CHAR(date_col,'YYYYMMDDHH24MISS') = '000000000000'
appears to be satisfactory to identify corrupted values stored in an Oracle DATE column. The addition of a function appears to be unnecessary. Checking for corrupted dates should be able to be done in a SQL SELECT statement, and not require a user to have CREATE FUNCTION privilege on the database.

This identifies invalid months
SELECT rowid,
pk_column,
DUMP(date_column, 1010) AS dump1
FROM table
WHERE TO_NUMBER(SUBSTR(DUMP(date_column, 1010), INSTR(DUMP( date_column, 1010),
',', 1, 2
) + 1,
INSTR(DUMP(date_column, 1010), ',', 1, 3) - (
INSTR(DUMP( date_column, 1010), ',', 1, 2) + 1
))) = 0;
Update using the same where clause, I found the month number was zero in these cases.

I'd go for a method that can be implemented as a check constraint, probably through checking that the date is in the range of legal values.
However, ask yourself also whether it is valid to have a date of 1066-10-14? It is a legal value, but you probably do not have invoices printed on that day, for example. So you might like to roll the invalid date check into a larger issue of what you really consider to be valid in the context of your application.

I had an SQL Error: ORA-01841: (full) year must be between -4713 and +9999, and not be 0
01841. 00000 - "(full) year must be between -4713 and +9999, and not be 0".
So to identify the rows that had a bad date I did the following.
declare
cursor mydates is select table_pk, your_date_col from table;
c_date table.your_date_col%type;
c_pk table.table_pk%type;
testrow table.your_date_col%type;
begin
open mydates;
loop
begin
fetch mydates into c_pk, c_date;
exit when mydates%notfound;
testrow := TO_TIMESTAMP(c_date,'YYYY-MM-DD HH24:MI:SS');
exception when others then
dbms_output.put_line('bad file: ' || c_pk);
end;
end loop;
close mydates;
end;
So all I did was create a cursor, loop through the elements and tested each one and display the identifier so I could easily find the bad rows.

Related

ORACLE QUERY to pass date as variable in TOAD

I'm new to oracle and trying to run a simple query to pass date dynamically
DEFINE startdate = TO_DATE(select TRUNC(LAST_DAY(ADD_MONTHS( max(nav_last_calc_dt) ,-1))+1) from tb);
DEFINE enddate = TO_DATE(select TRUNC(LAST_DAY(ADD_MONTHS(max(nav_last_calc_dt),0))) from tb);
begin
select Count(1)
FROM tb
WHERE DATE BETWEEN &startdate AND &enddate;
end;
I received the below error when executing using script (F5), TOAD script runner.
ORA-06550: line 4, column 78:
PL/SQL: ORA-00936: missing expression
ORA-06550: line 2, column 1:
PL/SQL: SQL Statement ignored
But when using SQL PLUS , it returned 7 as result. So I'm confused.
SQL*Plus isn't returning 7 as the result; you haven't completed the anonymous PL/SQL block, so it is showing you 7 as the next line number (as 'begin' is line 1 and 'end;' is line 6, and is waiting for input. If you enter a slash and hit return it will then execute the block; which will throw the same ORA-00936 error.
In both clients the problem is the DATE keyword - it's expecting that to be the start of a date literal, and doesn't see the rest of the literal value as it expects. That should be a column name, presumably:
WHERE nav_last_calc_dt DATE BETWEEN &startdate AND &enddate;
But the block will still fail, possibly for different reasons in the two clients; in SQL*Plus it will still get ORA-00936 because the defined value ends at the first space (which you can fix by enclosing in double quotes), and Toad may throw that error or complain that your select has no 'into' clause. (Or it might do something else; SQL Developer against 12cR1 is throwing an internal error.) The outer TO_DATE in your defined expressions is also not ideal - it will implicitly convert the date you have to a string and then convert that string back again to a real date, both using your session NLS settings; which might work, but isn't needed.
It's not clear why you are using PL/SQL there, or really why you are using a substitution variable - it's not really dynamic, it just makes the final statement a bit more obscure.
The date calculation also seems a bit complicated. It looks like you're trying to count rows from the last month with any data; and you're finding the first day of that month using add_months and last_day - which could be done more simply just by truncating the maximum date using the 'MM' date component:
select count(*)
from tb
where nav_last_calc_dt >= (select trunc(max(nav_last_calc_dt), 'MM') from tb)
Assuming the column used for the filter is `nav_last_calc_dt, and not some other column, you don't need an upper bound - you know the maximum date is in that month, so it has to be within the month.
If if was another column, with later dates, and you used between then you would exclude any values after midnight on the last day of that month. It's safer to use a full month range:
select count(*)
from tb
where some_date >= (select trunc(max(nav_last_calc_dt), 'MM') from tb)
and some_date < (select add_months(trunc(max(nav_last_calc_dt), 'MM'), 1) from tb)
which would find all values at or after midnight on the first day of the target month, and before midnight on the first day of the following month.
It might then be worth getting the maximum date once, but you could do that in a CTE or an inline view rather than via define, which wouldn't actually save you anything the way you are trying to use it - since both defined queries would be substituted into the query/block before it's executed.

Using oracle apex dynamic action to set a date time value that is concatenated

I have a date value in one page item and i have a time value in another. i want to concat the date with the time and set it to another item using a dynamic action. Structure as below
P_DATE = '01-01-2021' (item is a date with format mask dd-mm-yyyy)
P_TIME = '08:30 AM' (item is date with format mask HH:MIAM)
Query:
select to_date(to_char(to_date(P_DATE ,'DD-MON-YYYY'), 'DD-MON-YYYY') || ' '||
to_char(P_TIME,'HH:MIAM'),'DD-MON-YYYY HH:MIAM') a
from dual;
Desired Outcome: 01-01-2021 08:30 AM
Dynamic action is on change the P_DATE item then from an sql query, concat the P_DATE and P_TIME and set it to P_VALUE
When i run the select in sql developer with hardcoded values then it returns the correct stuff but when i try to set the value in the item with the concat date it giving me invalid number error sometimes and not a valid month.
Can you suggest the corrected way or an alternative way of doing this (maybe use a function)
THank you.
You can make that much simpler, example
with test as (
select '01-01-2021' d, '08:30 PM' t
from dual)
select to_date(d||' '|| t,'MM-DD-YYYY HH:MI PM')
from test;
What do you mean by "they are date items" ? Are they of the type "Date Picker" , do they have a source column of type "DATE" in a form, or do they map to a DATE column in the database ?
In APEX, all page items are basically strings. The frontend of the web application doesn't know about oracle datatypes so everything is treated as a plain string and during the processing the conversion is done. So that is how you should treat the page items, not as DATE data type like you would in SQL or PL/SQL. To concatenate a date string and a time string, you can just a plain concatenate without TO_CHAR. This can be done in plain PL/SQL, no need to SELECT FROM DUAL - that is just an unnecessary call to the SQL engine.
This is the "true" action on the change of P_VALUE. Tested on 21.1, so depending on your version there might be some attribute naming differences but it works the same.
Action: execute server side code.
Source:
:P_VALUE := :P_DATE ||' '||:P_TIME
Items to submit: P_DATE, P_TIME.
Items to return: P_VALUE
Since you're dealing with strings there is room for error here, you'll have to ensure proper error handling if the user input does not exactly match the format since that could generate invalid date values.
From comments:
the page items are both date items
Since they are dates, you can simply use:
SELECT p_date + (p_time - TRUNC(p_time)) AS a
FROM DUAL;

ORA-01843: not a valid month When where condition is changed

I am having problem with the following error
ORA-01843: not a valid month
The following query works well if I use
SELECT mx.work_order_no, mx.work_order_name, mx.comments
FROM max_orders mx
WHERE TO_DATE (wo_dt, 'dd/mm/rr') <= (TO_DATE (SYSDATE, 'dd/mon/rr') - 7)
However if I change where condition clause to
WHERE TO_DATE (wo_dt, 'dd/mm/rr') >= (TO_DATE (SYSDATE, 'dd/mon/rr') - 7)
I am having issue with
ORA-01843: not a valid month
What has caused this and how can I resolve this error?
Update 1
Underlying view
SELECT work_order_no,
work_order_name,
wo_dt,
comments
FROM (SELECT mx_master.work_order_no,
mx_master.work_order_name,
SUBSTR (mx_master.uom, 1, 15) wo_dt,
mx_master.remarks
FROM mx_wo_data mx_master)
SYSDATE is already a date. You should not be passing it into TO_DATE(). When you do that you're doing an implicit conversion to a string, and an explicit conversion back. Gordon Linoff already showed a better way to do that.
Based on the view definition you added, wo_dt is a string. You're expecting that to be in dd/mm/rr format. The error is telling you what you have values in that column which are not actually in that format, so you'll need to examine the data in the view or the underlying table to see which record(s) have incorrect data.
You could use something like this to either exclude the values that are not in the right format; or more usefully identify the bad values so they can be removed or corrected, e.g. with something like:
select * from max_orders
where my_to_date(wo_dt, 'dd/mm/rr') is null;
or from the underlying table:
select * from mx_wo_data
where my_to_date(substr(uom, 1, 8), 'dd/mm/rr') is null;
If you can't create a function then you can use the same logic in an anonymous block.
It's odd that changing the condition causes the error though, as your (implicit and explicit) conversions are applied before the condition is evaluated, and using the function means any index on that column can't be used; so (in the absence of any other filters) you should be doing a full table scan for both queries, the conversion should be applied to all values in the column before it's filtered, and you should get the error either way. So this doesn't really answer that aspect of the question.
I propose you create a stored function to identify the bad rows:
create function invalid_date(p_d in varchar2) return number as
v_d date;
begin
v_d := TO_DATE(p_d, 'dd/mm/rr');
return 0;
exception
when others then
return 1;
end;
/
select * from mx_orders where invalid_date(wo_dt)=1;
Don't convert sysdate to a date! Just use:
WHERE TO_DATE(wo_dt, 'dd/mm/rr') >= trunc(SYSDATE - 7)

Oracle: between dates statement gives error 'not valid month'

I'm not sure what I'm doing wrong here. I'm trying to build an oracle query that I will be executing from my php project via oci. I need to select all records between a specific date range.
In trying to get the syntax down, I wrote out this test query:
SELECT * FROM SHIPPED
WHERE user_seq_id = 381 AND
LOT_DATE BETWEEN TO_DATE('05/27/2014', 'MM/DD/YYYY')
AND TO_DATE('06/03/2014','MM/DD/YYYY');
This syntax seems like it should work but it's not. I'm definitely not an oracle developer so I'm positive I"m misunderstanding something. When I've looked at similar posts I haven't found anything that would point to what I'm doing wrong.
This is a rather tricky error. The problem would occur if LOT_DATE were stored as a character string rather than a date -- and the string contained invalid data.
By explicitly converting the right hand side of the comparison to dates, the comparison is attempted by converting the field to a date. And there is an error.
The fix is to fix the data in the field. if something is in a field called "date", then it should probably have a date data type.
For identifying unsupported string value for date - You can create a PL/SQL function which accepts string and validate for correct date format, sample of function:
CREATE OR REPLACE FUNCTION VERIFY_DATE(v_date IN VARCHAR2) RETURN NUMBER IS
v_date1 DATE;
BEGIN
select to_date(v_date) into v_date1 from dual;
RETURN 1;
Exception WHEN Others THEN
RETURN 0;
END;
Now, identify rows which are having invalid string value for which you should correct to run your query:
select * from
(
select VERIFY_DATE(LOT_DATE) DateVerified,s.* from SHIPPED s
)
where
DateVerified=0

Why are these NVL() statements wrong in SQL?

SELECT inv_no,NVL2(inv_amt,inv_date,'Not Available')
FROM invoice;
SELECT inv_no,NVL2(inv_amt,inv_amt*.25,'Not Available') FROM invoice;
getting
ORA-01858: a non-numeric character was found where a numeric was expected
ORA-01722: invalid number
respectively
Please suggest what are the datatypes I can give in expr1 and expr2.
Also Please tell me how this is right?
SELECT inv_no,NVL2(inv_date,sysdate-inv_date,sysdate)
FROM invoice
The 2nd and 3rd parameters to NVL2 should be the same datatype. So, assuming inv_date is a DATE, you'd need to have that as a varchar2 like;
SELECT inv_no, NVL2(inv_amt, to_char(inv_date, 'dd-mon-yyyy hh24:mi:ss'), 'Not Available') FROM ...
or whatever format string you wanted. Otherwise Oracle will convert the 3rd parameter 'Not Available' to match the 2nd parameter's data type. This will try to convert 'Not Available' to a date and crash.
Similarly in the 2nd example, you have to convert the inv_amt*.25 to a char viato_char, e.g. to_char(inv_amt*.25).
Your first 2 examples attempt to have a date / numeric and text results for the same field. This will cause an error when Oracle attempts to convert this text to either types. You'll need to use the to_char(field) function on the date / numeric fields to convert them to text.
Lastly a date is in fact a number added to a databases base date. For example a date is a number of base day and the decimal it has is the ratio of a day, e.g. 0.5 is 12 hours, and the database has a base date, e.g. 01-Jan-1900 or 01-Jan-2000. This is why when you do date - date the result is a number and a date can also be represented as a number.
Refer to: https://docs.oracle.com/cd/B19306_01/server.102/b14200/functions106.htm
The 2nd argument should be either character or numeric. The 2nd argument gives datatype to which the 3rd argument will be implictly converted. So:
1. The first one SELECT inv_no,NVL2(inv_amt,inv_date,'Not Available') FROM invoice; is incorrect as you cannot have DATE as a 2nd argument.
The same way:
SELECT NVL2(NULLIF(1, 1), SYSDATE, 'A') FROM DUAL; -- FAILS;
SELECT NVL2(NULLIF(1, 0), SYSDATE, 'A') FROM DUAL; -- FAILS;
SELECT NVL2(NULLIF(1, 1), 'A', SYSDATE) FROM DUAL; -- PASS; returns SYSDATE (as char datatype)
SELECT NVL2(NULLIF(1, 0), 'A', SYSDATE) FROM DUAL; -- PASS; returns 'A';
2. The second is invalid too: SELECT inv_no,NVL2(inv_amt,inv_amt*.25,'Not Available') FROM invoice; Oracle tries to implicitly convert 3rd argument to the datatype of the 2nd arugment, meaning it tries to convert 'Not available' to numeric value.
It will work when you swap 2nd and 3rd argument:
SELECT inv_no,NVL2(inv_amt,'Not Available',inv_amt*.25) FROM invoice;
inv_amt*.25 will be implicitly converted to character data; This example is useless as it will get you NULL*.25 making it NULL.
What you can do however is you can make both parameters as characters by converting numeric result to character:
SELECT inv_no,NVL2(inv_amt,TO_CHAR(inv_amt*.25), 'Not Available') FROM invoice;
It will make some sense particularly when you want to display currency.
3. The third one is more complex
The only way I can explain is that the 2nd argument gets converted to character before dealing with the third one. That way the third one will get converted to characters too. This would make sense given the two cases:
SELECT NVL2(2, SYSDATE - TO_DATE('10-JAN-83'), SYSDATE) from dual; -- PASSES
SELECT NVL2(2, 2.2, SYSDATE) from dual; -- FAILS.
Finally: I would advise to stick to Oracle's documentation and use explicit conversion.