I am working on some analysis tools and I have a specific situation where I have a value that is represented using the HEXTORAW Oracle function as follows:
HEXTORAW('7876031b0d233900786450')
This value represents what would otherwise be written as TO_TIMSTAMP('2018-03-27 12:34:56.00789'). So far I have found two ways to attempt to convert this HEXTORAW value:
Using dbms_stats.convert_raw_to_date
Using dbms_stats.convert_raw_value in a PL/SQL block
In both cases, the return value doesn't have the fractal seconds portion of the timestamp and I suspect this is likely due to the underlying calls relying on a DATE data type rather than a true TIMESTAMP data type.
Does anyone know if there is another PL/SQL package function that could be used to decode the raw value back to a complete timestamp that includes the fractal seconds?
There is not a function that converts a hex-string to a TIMESTAMP but you can easily calculate the TIMESTAMP value.
Convert the first 7 bytes to a DATE and then cast it to a TIMESTAMP(9) (which will have fractional seconds of 0) and then you can convert the last 4 bytes to a number, which represents the number of nanoseconds and then add that to the timestamp:
SELECT CAST(
DBMS_STATS.CONVERT_RAW_TO_DATE(HEXTORAW(SUBSTR(hex_value,1,14)))
AS TIMESTAMP(9)
)
+ INTERVAL '1' SECOND(9)
* TO_NUMBER(HEXTORAW(SUBSTR(hex_value,15,8)), 'XXXXXXXX') / 1e9
AS timestamp_val,
hex_value
FROM (
SELECT '7876031b0d233900786450' AS hex_value FROM DUAL
)
Outputs:
TIMESTAMP_VAL
HEX_VALUE
2018-03-27 12:34:56.007890000
7876031b0d233900786450
and
CREATE TABLE table_name (value TIMESTAMP(9));
INSERT INTO table_name (value) VALUES (TIMESTAMP '2018-03-27 12:34:56.007890000');
SELECT value, DUMP(value,16) FROM table_name
Outputs:
VALUE
DUMP(VALUE,16)
2018-03-27 12:34:56.007890000
Typ=180 Len=11: 78,76,3,1b,d,23,39,0,78,64,50
Which has the same timestamp and same byte values.
If you want to wrap it into a function then:
CREATE FUNCTION hex_to_timestamp(
hex_value IN VARCHAR2
) RETURN TIMESTAMP DETERMINISTIC
IS
BEGIN
RETURN CAST(
DBMS_STATS.CONVERT_RAW_TO_DATE(HEXTORAW(SUBSTR(hex_value,1,14)))
AS TIMESTAMP
)
+ INTERVAL '1' SECOND
* TO_NUMBER(HEXTORAW(SUBSTR(hex_value,15,8)), 'XXXXXXXX') / 1e9;
END;
/
db<>fiddle here
Update
Perhaps the "simplest" method is to use something that will already convert a byte string to a timestamp. In this case, the Java class oracle.sql.TIMESTAMP has a constructor that takes a byte array (and there is a similar constructor for oracle.sql.TIMESTAMPTZ).
Therefore, if you have Java enabled in the database, you can use a small class to convert the hex-string to a byte array and then to wrap the constructors in functions:
CREATE AND COMPILE JAVA SOURCE NAMED HexToTimestampConverter AS
import oracle.sql.TIMESTAMP;
import oracle.sql.TIMESTAMPTZ;
public class HexToTimestampConverter {
public static byte[] hexStringToByteArray(String s) {
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+ Character.digit(s.charAt(i+1), 16));
}
return data;
}
public static TIMESTAMP hexToTimestamp(final String hex)
{
return new TIMESTAMP(hexStringToByteArray(hex));
}
public static TIMESTAMPTZ hexToTimestamptz(final String hex)
{
return new TIMESTAMPTZ(hexStringToByteArray(hex));
}
}
Then you can create an SQL function to call the Java code:
CREATE FUNCTION hex_to_timestamp( in_value IN VARCHAR2 ) RETURN TIMESTAMP
AS LANGUAGE JAVA NAME 'HexToTimestampConverter.hexToTimestamp(java.lang.String) return oracle.sql.TIMESTAMP';
/
And for TIMESTAMP WITH TIME ZONE:
CREATE FUNCTION hex_to_timestamptz( in_value IN VARCHAR2 ) RETURN TIMESTAMP WITH TIME ZONE
AS LANGUAGE JAVA NAME 'HexToTimestampConverter.hexToTimestamptz(java.lang.String) return oracle.sql.TIMESTAMPTZ';
/
(Note: you probably want to add some error checking to check that the string is hexadecimal and of the correct length.)
db<>fiddle here
Related
I am trying to create function of existing SELECT query.
SELECT uta.StartDate, uta.EndDate FROM user_timesheets_absence uta
WHERE uta.UserID = 353
AND uta.Approved = 1
AND DATE '2020-06-06' BETWEEN TO_DATE(uta.StartDate,'YYYY-MM-DD')
AND TO_DATE(uta.EndDate,'YYYY-MM-DD') + INTERVAL '1' DAY
When I execute this query, I get correct result in output StartDate and EndDate.
But problem is when I write function and try to compile I get error
Error(724,8): PL/SQL: ORA-00936: missing expression
FUNCTION GET_AVAILABE_USER_PER_DATES(p_userId IN INT,p_dateFormat IN VARCHAR2)
RETURN SYS_REFCURSOR IS
rc SYS_REFCURSOR;
BEGIN
OPEN rc FOR
SELECT uta.StartDate, uta.EndDate FROM user_timesheets_absence uta
WHERE uta.UserID = p_userId
AND uta.Approved = 1
AND DATE p_dateFormat BETWEEN TO_DATE(uta.StartDate,'YYYY-MM-DD')
AND TO_DATE(uta.EndDate,'YYYY-MM-DD') + INTERVAL '1' DAY;
RETURN rc;
END GET_AVAILABE_USER_PER_DATES;
Compiler gives me error in following line
AND DATE p_dateFormat BETWEEN TO_DATE(uta.StartDate,'YYYY-MM-DD')
I try to understand what I made wrong, where is mistake but I couldn't ?
What is wrong here ? What i make wrong can someone tell me ?
UPDATE
FUNCTION ABSENCE_EXIST_FOR_DATE(p_userId IN INT,p_dateFormat IN DATE)
RETURN SYS_REFCURSOR IS
rc SYS_REFCURSOR;
BEGIN
OPEN rc FOR
SELECT uta.StartDate, uta.EndDate FROM user_timesheets_absence uta
WHERE uta.UserID = p_userId
AND uta.Approved = 1
AND p_dateFormat BETWEEN TO_DATE(uta.StartDate,'YYYY-MM-DD')
AND TO_DATE(uta.EndDate,'YYYY-MM-DD') + INTERVAL '1' DAY;
RETURN rc;
END ABSENCE_EXIST_FOR_DATE;
Error gone, but unfortunetlly when I execute function I get error message
ORA-01861: literal does not match format string
01861. 00000 - "literal does not match format string"
*Cause: Literals in the input must be the same length as literals in
the format string (with the exception of leading whitespace). If the
"FX" modifier has been toggled on, the literal must match exactly,
with no extra whitespace.
*Action: Correct the format string to match the literal.
You must replace
DATE p_dateFormat
with the appropriate conversion of the VARCHAR2 parameter p_dateFormat to the DATE type
to_char(p_dateFormat,'yyyy-mm-dd')
Adapt the the format if appropriate or use DATE parameter in the function.
Than you would simple constraint
FUNCTION GET_AVAILABE_USER_PER_DATES(p_userId IN INT,p_dateFormat IN DATE)
...
AND p_dateFormat BETWEEN TO_DATE(uta.StartDate,'YYYY-MM-DD')
AND TO_DATE(uta.EndDate,'YYYY-MM-DD') + INTERVAL '1' DAY
Check if the name p_dateFormat is the best possible name for a parameter with a DATE value (or a string with a DATE value) ...
To avoid ORA-01861: literal does not match format string use the ON CONVERSION ERROR clause as follows (Oracle 12 and up is required)
FUNCTION GET_AVAILABE_USER_PER_DATES(p_userId IN INT,p_dateFormat IN DATE)
...
AND p_dateFormat BETWEEN TO_DATE(uta.StartDate default null on conversion error,'YYYY-MM-DD')
AND TO_DATE(uta.EndDate default null on conversion error,'YYYY-MM-DD') + INTERVAL '1' DAY
If the column EndDate or StartDate contain invalid data, such as today or 2020-99-01 the conversion will not fail with error, but returns null, is the record will not be selected (which is probably what you want).
I need a query (in Oracle), that will be inside a stored procedure, where I can get a sum of the Amount value of Table REV.
The YEAR and one MONTH will be received as a parameter in the stored procedure, as YY and MM.
What I want is to sum the amount values since the 1st month of the year UP to the MM passed in the argument.
So
if MM in the argument is 02, I want to take the sum of amounts of months 01 + 02
if MM in the argument is 05, I want to take the sum of amounts of months 01+02+03+04+05
So MM is the last month to be summed.
How can I make this in the most efficient and elegant way?
CREATE OR REPLACE PROCEDURE "GET_YTD_AMOUNT" (YY in VARCHAR,
MM in VARCHAR)
select
ACT.LABEL ,
R.YEAR,
R.MONTH,
sum(R.AMOUNT)
from
ACTIVITY ACT,
REV R
where
R.YEAR=YEAR and
R.MONTH ??
R.ID_CODE = ACT.ID_CODE
I'd prefer using numeric variables rather than strings for such cases.
In your case ;
considering your year and month parameters are of string type, you need a to_number() conversion with less than or equal to operator
to_number(R.MONTH) <= to_number(i_month)
add an out parameter o_amount to return the result you get
of course, you need to convert your SQL format containing explicit
joins
better to define parameters ( or local variables ) by their type
within the tables in which they're contained. Btw, I didn't define o_amount by rev.amount%type against probability of sum() aggregation might exceed the precision of numeric value provided it's defined as so within the table.
So,use :
CREATE OR REPLACE PROCEDURE GET_YTD_AMOUNT(
i_year in rev.year%type,
i_month in rev.month%type,
o_amount out number
) IS
BEGIN
select sum(r.amount)
into o_amount
from activity a
join rev r
on r.id_code = a.id_code
where r.year = i_year
and to_number(r.month) <= to_number(i_month);
END;
/
You can use less than equal to:
select
ACT.LABEL ,
R.YEAR,
Max(R.MONTH) || '-' || Max(R.MONTH) as months_from_to
sum(R.AMOUNT)
from
ACTIVITY ACT,
REV R
where
R.YEAR= YY and -- it should be YY
R.MONTH <= MM -- less than equal to with MM
R.ID_CODE = ACT.ID_CODE
Group by ACT.LABEL ,
R.YEAR
Note: You must re-design your DB to store dates in date data type.
Cheers!!
I am trying to sum INTERVAL. E.g.
SELECT SUM(TIMESTAMP1 - TIMESTAMP2) FROM DUAL
Is it possible to write a query that would work both on Oracle and SQL Server? If so, how?
Edit: changed DATE to INTERVAL
I'm afraid you're going to be out of luck with a solution which works in both Oracle and MSSQL. Date arithmetic is something which is very different on the various flavours of DBMS.
Anyway, in Oracle we can use dates in straightforward arithmetic. And we have a function NUMTODSINTERVAL which turns a number into a DAY TO SECOND INTERVAL. So let's put them together.
Simple test data, two rows with pairs of dates rough twelve hours apart:
SQL> alter session set nls_date_format = 'dd-mon-yyyy hh24:mi:ss'
2 /
Session altered.
SQL> select * from t42
2 /
D1 D2
-------------------- --------------------
27-jul-2010 12:10:26 27-jul-2010 00:00:00
28-jul-2010 12:10:39 28-jul-2010 00:00:00
SQL>
Simple SQL query to find the sum of elapsed time:
SQL> select numtodsinterval(sum(d1-d2), 'DAY')
2 from t42
3 /
NUMTODSINTERVAL(SUM(D1-D2),'DAY')
-----------------------------------------------------
+000000001 00:21:04.999999999
SQL>
Just over a day, which is what we would expect.
"Edit: changed DATE to INTERVAL"
Working with TIMESTAMP columns is a little more labourious, but we can still work the same trick.
In the following sample. T42T is the same as T42 only the columns have TIMESTAMP rather than DATE for their datatype. The query extracts the various components of the DS INTERVAL and converts them into seconds, which are then summed and converted back into an INTERVAL:
SQL> select numtodsinterval(
2 sum(
3 extract (day from (t1-t2)) * 86400
4 + extract (hour from (t1-t2)) * 3600
5 + extract (minute from (t1-t2)) * 600
6 + extract (second from (t1-t2))
7 ), 'SECOND')
8 from t42t
9 /
NUMTODSINTERVAL(SUM(EXTRACT(DAYFROM(T1-T2))*86400+EXTRACT(HOURFROM(T1-T2))*
---------------------------------------------------------------------------
+000000001 03:21:05.000000000
SQL>
At least this result is in round seconds!
Ok, after a bit of hell, with the help of the stackoverflowers' answers I've found the solution that fits my needs.
SELECT
SUM(CAST((DATE1 + 0) - (DATE2 + 0) AS FLOAT) AS SUM_TURNAROUND
FROM MY_BEAUTIFUL_TABLE
GROUP BY YOUR_CHOSEN_COLUMN
This returns a float (which is totally fine for me) that represents days both on Oracle ant SQL Server.
The reason I added zero to both DATEs is because in my case date columns on Oracle DB are of TIMESTAMP type and on SQL Server are of DATETIME type (which is obviously weird). So adding zero to TIMESTAMP on Oracle works just like casting to date and it does not have any effect on SQL Server DATETIME type.
Thank you guys! You were really helpful.
You can't sum two datetimes. It wouldn't make sense - i.e. what does 15:00:00 plus 23:59:00 equal? Some time the next day? etc
But you can add a time increment by using a function like Dateadd() in SQL Server.
In SQL Server as long as your individual timespans are all less than 24 hours you can do something like
WITH TIMES AS
(
SELECT CAST('01:01:00' AS DATETIME) AS TimeSpan
UNION ALL
SELECT '00:02:00'
UNION ALL
SELECT '23:02:00'
UNION ALL
SELECT '17:02:00'
--UNION ALL SELECT '24:02:00' /*This line would fail!*/
),
SummedTimes As
(
SELECT cast(SUM(CAST(TimeSpan AS FLOAT)) as datetime) AS [Summed] FROM TIMES
)
SELECT
FLOOR(CAST(Summed AS FLOAT)) AS D,
DATEPART(HOUR,[Summed]) AS H,
DATEPART(MINUTE,[Summed]) AS M,
DATEPART(SECOND,[Summed]) AS S
FROM SummedTimes
Gives
D H M S
----------- ----------- ----------- -----------
1 17 7 0
If you wanted to handle timespans greater than 24 hours I think you'd need to look at CLR integration and the TimeSpan structure. Definitely not portable!
Edit: SQL Server 2008 has a DateTimeOffset datatype that might help but that doesn't allow either SUMming or being cast to float
I also do not think this is possible. Go with custom solutions that calculates the date value according to your preferences.
You can also use this:
select
EXTRACT (DAY FROM call_end_Date - call_start_Date)*86400 +
EXTRACT (HOUR FROM call_end_Date - call_start_Date)*3600 +
EXTRACT (MINUTE FROM call_end_Date - call_start_Date)*60 +
extract (second FROM call_end_Date - call_start_Date) as interval
from table;
You Can write you own aggregate function :-). Please read carefully http://docs.oracle.com/cd/B19306_01/appdev.102/b14289/dciaggfns.htm
You must create object type and its body by template, and next aggregate function what using this object:
create or replace type Sum_Interval_Obj as object
(
-- Object for creating and support custom aggregate function
duration interval day to second, -- In this property You sum all interval
-- Object Init
static function ODCIAggregateInitialize(
actx IN OUT Sum_Interval_Obj
) return number,
-- Iterate getting values from dataset
member function ODCIAggregateIterate(
self IN OUT Sum_Interval_Obj,
ad_interval IN interval day to second
) return number,
-- Merge parallel summed data
member function ODCIAggregateMerge(
self IN OUT Sum_Interval_Obj,
ctx2 IN Sum_Interval_Obj
) return number,
-- End of query, returning summary result
member function ODCIAggregateTerminate
(
self IN Sum_Interval_Obj,
returnValue OUT interval day to second,
flags IN number
) return number
)
/
create or replace type body Sum_Interval_Obj is
-- Object Init
static function ODCIAggregateInitialize(
actx IN OUT Sum_Interval_Obj
) return number
is
begin
actx := Sum_Interval_Obj(numtodsinterval(0,'SECOND'));
return ODCIConst.Success;
end ODCIAggregateInitialize;
-- Iterate getting values from dataset
member function ODCIAggregateIterate(
self IN OUT Sum_Interval_Obj,
ad_interval IN interval day to second
) return number
is
begin
self.duration := self.duration + ad_interval;
return ODCIConst.Success;
exception
when others then
return ODCIConst.Error;
end ODCIAggregateIterate;
-- Merge parallel calculated intervals
member function ODCIAggregateMerge(
self IN OUT Sum_Interval_Obj,
ctx2 IN Sum_Interval_Obj
) return number
is
begin
self.duration := self.duration + ctx2.duration; -- Add two intervals
-- return = All Ok!
return ODCIConst.Success;
exception
when others then
return ODCIConst.Error;
end ODCIAggregateMerge;
-- End of query, returning summary result
member function ODCIAggregateTerminate(
self IN Sum_Interval_Obj,
returnValue OUT interval day to second,
flags IN number
) return number
is
begin
-- return = All Ok, too!
returnValue := self.duration;
return ODCIConst.Success;
end ODCIAggregateTerminate;
end;
/
-- You own new aggregate function:
CREATE OR REPLACE FUNCTION Sum_Interval(
a_Interval interval day to second
) RETURN interval day to second
PARALLEL_ENABLE AGGREGATE USING Sum_Interval_Obj;
/
Last, check your function:
select sum_interval(duration)
from (select numtodsinterval(1,'SECOND') as duration from dual union all
select numtodsinterval(1,'MINUTE') as duration from dual union all
select numtodsinterval(1,'HOUR') as duration from dual union all
select numtodsinterval(1,'DAY') as duration from dual);
Finally You can create SUM function, if you want.
Note: the patient data displayed below is "dummy" data that I made up. It is not the actual information for an actual patient.
I have a function with the following conversion in it:
Declare #bdate date
set #bdate = CONVERT ( date , left(#dob,8) , 112 )
If I just run this in a query window, it converts the date fine
select CONVERT(date, left('19900101', 8), 112) //returns a good date
But if I step through a scalar function with the same code in it in visual studio I get an error...
Declare #bdate date
set #bdate = CONVERT ( date , left(#pidPatientDob,8) , 112 )
throws...
Running [dbo].[getAgeAtTestDate] ( #obxTestDate = '20120101',
#pidPatientDob = '19900101' ).
Conversion failed when converting date and/or time from character
string. Invalid attempt to read when no data is present.
Why does it work in the query window but not in the function? It seems like the parameters are getting filled properly in the function.
Here is the full text of the function, which is returning null (I think because of the error)
ALTER FUNCTION [dbo].[getAgeAtTestDate]
(
-- Add the parameters for the function here
#obxTestDate as nvarchar(50), #pidPatientDob as nvarchar(50)
)
RETURNS int
AS
BEGIN
Declare #bdate date
set #bdate = CONVERT ( date , left(#pidPatientDob,8) , 112 )
Declare #testDate date
set #testDate = CONVERT ( date , left(#testDate,8) , 112 )
-- Return the result of the function
RETURN datediff(mm, #testDate, #bdate)
END
Your parameter is called obxTestDate, not testDate, so change;
set #testDate = CONVERT ( date , left(#testDate,8) , 112 )
into
set #testDate = CONVERT ( date , left(#obxTestDate,8) , 112 )
and things will work better.
As a side note, I think you reversed the DATEDIFF too, the start date should come before the end date;
RETURN datediff(mm, #bdate, #testDate)
I'm attempting to write a BeforeReport Trigger that checks to see if a user is accessing report data within 120 seconds of creation.
My code is as follows:
function BeforeReport return boolean is
ENTRYDATE TIMESTAMP;
SERVERDATE TIMESTAMP;
DIFFSECONDS NUMBER;
begin
/*Checks to see if user is accessing the data within 120 seconds of creation in database*/
VALIDTIMELENGTH := 120;
SELECT SYS_DATE INTO ENTRYDATE FROM WOS_REPORT_PARAM WHERE SEQUENCE_NUM=UPPER(:SEQUENCENUM) AND PARAMETER='year';
SERVERDATE := CURRENT_TIMESTAMP;
DIFFSECONDS := ((EXTRACT(YEAR FROM ENTRYDATE)- EXTRACT(YEAR FROM SERVERDATE))*31536000) +
((EXTRACT(MONTH FROM ENTRYDATE)- EXTRACT(MONTH FROM SERVERDATE))*2592000) +
((EXTRACT(DAY FROM ENTRYDATE) - EXTRACT(DAY FROM SERVERDATE))*86400) +
((EXTRACT(HOUR FROM ENTRYDATE) - EXTRACT(HOUR FROM SERVERDATE))*3600) +
((EXTRACT(MINUTE FROM ENTRYDATE) - EXTRACT(MINUTE FROM SERVERDATE))*60) +
(EXTRACT(SECOND FROM ENTRYDATE) - EXTRACT(SECOND FROM SERVERDATE));
IF DIFFSECONDS > VALIDTIMELENGTH THEN RETURN(FALSE);
ELSE
return (TRUE);
END IF;
END;
The problem is, my code seems to return true regardless of how much time has gone by. Am I not implementing my seconds/minutes code correctly? Oddly enough , I experimented with making both returns (FALSE) and it still proceeded to pass the report! Oracle Reports is so inconsistent that it's infuriating. Any response is greatly appreciated.
If wos_report_param.sys_date is a TIMESTAMP (note that it's rather confusing to name a column sys_date when there is a sysdate function that returns a DATE), and assuming that wos_report_param.sys_date is a timestamp in the past, your logic can be simplified quite a bit
IF( current_timestamp - entrydate > numtodsinterval( validTimeLength, 'second' ) )
THEN
RETURN true;
ELSE
RETURN false;
END IF;
If my guess is correct and wos_report_param.sys_date is a timestamp in the past, then the diffseconds you are computing will be a negative number which explains why your procedure always returns false. You need to subtract the earlier date from the later date in order to yield a positive rather than a negative interval. Or you could throw an abs around your diffseconds calculation.