DB2 UDF to return added date? - sql

I am trying to make a DB2 UDF that takes input a date and returns new date with some months added to it. The query syntax for this step is to use
select date + 12 months from sysibm.sysdummy1;
Inside the UDF I can't get this to work
monthsToAdd INTEGER;
SET monthsToAdd=4;
set result= inputDate + monthsToAdd + MONTHS;
return result;
It says
SQL State: 42816
Vendor Code: -182
Message: [SQL0182] A date, time, or timestamp expression not valid. Cause . . . . . : One of the following has occurred: -- An operand of addition is a date and the other is not a date duration. -- An operand of addition is a time and the other is not a time duration. -- An operand of addition is a timestamp and the other is not a duration. -- An operand of subtraction is a date and the other is not a date, character, or date duration. -- An operand of subtraction is a time and the other is not a time, character, or time duration. -- An operand of subtraction is a timestamp and the other is not a timestamp, character, or duration. Recovery . . . : Correct the arithmetic expression so that it contains a valid date, time, or timestamp expression. Try the request again.

It's a simple fix:
set result = inputDate + monthsToAdd MONTHS;

As actually scripted on a v5r3 DB2 for i5/OS [and also with DB2 for IBM i 7.3]:
create function add_months
( inputDate DATE
, monthsToAdd INTEGER
) returns DATE
language SQL DETERMINISTIC
RETURN inputDate + monthsToAdd MONTHS
select add_months(date'2016-04-12', 6)
from sysibm.sysdummy1
-- likeness of a report from above query follows:
ADD_MONTHS
2016-10-12

Related

Converting decimal to Date

I have a column with dates formatted as decimals, for example: 20,210,830.
I want to convert this number to date format as 08/30/2021
I have tried to use convert and the database shoots me an error that convert is not a valid function. Cast seems to work but, only returns a null value every time.
This statement will validate:
SELECT CAST(CAST(CONTCLMPDTE AS VARCHAR(8)) AS DATE)
FROM CMSFIL.JCTDSC AS COMPLDATE
This statement works but, just outputs null. For background I am querying from a Db2 database.
My ultimate goal is to use this converted date to grab the difference from the current day.
Such as
DAY(CURRENT_DATE) - DAY(COMPLDATE)
Converting it to a date, you cqan do it like this
CREATE TABLE JCTDSC (
CONTCLMPDTE varchar(10)
);
INSERT INTO JCTDSC VALUES ('20,220,830')
SELECT date(to_date(REPLACE(CONTCLMPDTE,',',''),'YYYYMMDD')) FROM JCTDSC AS COMPLDATE
1
2022-08-30
fiddle
So after a long couple days and almost pulling my hair out, here is what worked for me.
SELECT date(substr(CONTCLMPDTE,1,4)||'-'||substr(CONTCLMPDTE,5,2)||'-'||substr(CONTCLMPDTE,7,2)) FROM JCTDSC WHERE COMPANYNUMBER={Company Number} AND JOBNUMBER={Job Number} LIMIT 1
This formatted from yyyymmdd to mm/dd/yyyy. It also worked for finding the days between current_date and CONTCLMPDTE using this code.
DAYS(CURRENT_DATE) - DAYS({COntract Compl Date Formatted})
Thank you all for your help!
You probably get an error because you have some INT / DECIMAL value which can't be converted to a date using this pattern.
The solution is to create some "safe" conversion function "eating" errors like below.
CREATE FUNCTION DATE_SAFE (P_DT INT)
RETURNS DATE
CONTAINS SQL
NO EXTERNAL ACTION
DETERMINISTIC
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
RETURN CAST (NULL AS DATE);
END;
RETURN DATE (TO_DATE (CAST (P_DT AS CHAR (8)), 'YYYYMMDD'));
END
Usage:
SELECT
CONTCLMPDTE
--, DATE (TO_DATE (CAST (CONTCLMPDTE AS CHAR (8)), 'YYYYMMDD'))
, DATE_SAFE (CONTCLMPDTE)
FROM (VALUES 0, 20220830) T (CONTCLMPDTE)
The function returns NULL if the corresponding INT can't be converted to a DATE, and no error is thrown as in case, when you uncomment the commented out line with built-in functions only.
The value just need to be converted into a string with a date format. Then you can use the date() function to convert to date.
create table qtemp/dec_vals (
col1 decimal(8,0) );
insert into qtemp/dec_vals
values (20200830), (20200831), (20200901), (20200902), (20200903), (20200904), (20200905), (20200906);
select date(substr(char(col1), 5, 2) || '/' || substr(char(col1), 7, 2) || '/' || substr(char(col1), 1, 4)) from qtemp/dec_vals;

How do I write a function in plpgsql that compares a date with a timestamp without time zone?

I want to write a function that returns a table with all the rows between firstDate and lastDate. The rows have datatype timestamp without time zone They also have to be of a specific node id.
This is my function:
CREATE OR REPLACE FUNCTION get_measurements_by_node_and_date(nodeID INTEGER, firstDate date, lastDate date)
RETURNS TABLE (measurement_id INTEGER, node_id INTEGER, carbon_dioxide DOUBLE PRECISION,
hydrocarbons DOUBLE PRECISION, temperature DOUBLE PRECISION,
humidity DOUBLE PRECISION,
air_pressure DOUBLE PRECISION,
measurement_timestamp timestamp without time zone ) AS
$$
DECLARE
sql_to_execute TEXT;
BEGIN
SELECT 'SELECT measurements_lora.id,
measurements_lora.node_id,
measurements_lora.carbon_dioxide,
measurements_lora.hydrocarbons,
measurements_lora.temperature,
measurements_lora.humidity,
measurements_lora.air_pressure,
measurements_lora.measurement_timestamp AS measure
FROM public.measurements_lora
WHERE measurements_lora.measurement_timestamp <= '||lastDate||'
AND measurements_lora.measurement_timestamp >= '||firstDate||'
AND measurements_lora.node_id = '||nodeID||' '
INTO sql_to_execute;
RETURN QUERY EXECUTE sql_to_execute;
END
$$ LANGUAGE plpgsql;
The column measurement_timestamp is of type timestamp without time zone and is formatted like yy-mm-dd hh-mm-ss
When I run SELECT * FROM get_measurements_by_node_and_date(1, '2020-5-1', '2020-5-24')
I get the following error:
ERROR: operator does not exist: timestamp without time zone <= integer
LINE 10: ... WHERE measurements_lora.measurement_timestamp <= 2020-05...
I don't get why it says "integer", because I clearly defined firstDate and lastDate as type date.
What Pavel said.
What's more, nothing indicates a need for PL/pgSQL to begin with. A plain (prepared) SELECT statement can do it. Or an SQL function, if you want to persist it in the database. See:
Difference between language sql and language plpgsql in PostgreSQL functions
And concerning:
with all the rows between firstDate and lastDate
Define inclusive / exclusive upper / lower bound precisely to avoid surprising corner case results. When comparing a timestamp column to a date, the latter is coerced to the timestamp signifying the first instance of the day: YYYY.MM.DD 00:00:00.
Your query says:
measurement_timestamp <= lastDate AND measurement_timestamp >= firstDate
... which would include all of firstDate, but exclude all of lastDate except for the first (common) instance at 00:00. Typically not what you want. Given your formulation, I suppose this is what you really want:
CREATE OR REPLACE FUNCTION get_measurements_by_node_and_date(node_id integer
, firstDate date
, lastDate date)
RETURNS TABLE (measurement_id integer
, node_id integer
, carbon_dioxide float8
, hydrocarbons float8
, temperature float8
, humidity float8
, air_pressure float8
, measurement_timestamp timestamp)
LANGUAGE sql STABLE AS
$func$
SELECT m.id
, m.node_id
, m.carbon_dioxide
, m.hydrocarbons
, m.temperature
, m.humidity
, m.air_pressure
, m.measurement_timestamp -- AS measure -- just documentation
FROM public.measurements_lora m
WHERE m.node_id = _node_id
AND m.measurement_timestamp >= firstDate::timestamp
AND m.measurement_timestamp < (lastDate + 1)::timestamp -- ①!
$func$;
① This includes all of lastDate, and efficiently. You can just add / subtract an integer value to / from a date to add / subtract days. The explicit cast to ::timestamp is optional as date would be coerced in the expression automatically. But since we are trying to clear up confusion here ...
Related:
How do I determine the last day of the previous month using PostgreSQL?
Aside 1:
The column measurement_timestamp is of type timestamp without time zone and is formatted like yy-mm-dd hh-mm-ss
No. timestamp values are not formatted, period. They are just timestamp values (internally stored as the number of microseconds since the epoch). Display is completely separate from the value and can be adjusted in a hundred and one ways without changing the value. Get rid of this misconception to better understand what's going on. See:
Ignoring time zones altogether in Rails and PostgreSQL
Aside 2:
About the devious nature of SQL BETWEEN:
How to add a day/night indicator to a timestamp column?
Aside 3:
Consider legal, lower-case identifiers in Postgres. first_date instead of firstDate. See:
Are PostgreSQL column names case-sensitive?
Related:
PostgreSQL function returning a data cube
Postgresql trying to use execute format in a function but getting column not found error when giving string format in coalesce
In this case is good to write executed query first. I try to reduce your example:
CREATE OR REPLACE FUNCTION public.foo(d date)
RETURNS TABLE(o integer)
LANGUAGE plpgsql
AS $function$
declare q text;
begin
q := 'select 1 from generate_series(1,100) where current_timestamp <= ' || d ;
raise notice '%', q;
return query execute q;
end;
$function$
postgres=# select * from foo('2020-05-25');
NOTICE: 00000: select 1 from generate_series(1,100) where current_timestamp <= 2020-05-25
LOCATION: exec_stmt_raise, pl_exec.c:3826
ERROR: 42883: operator does not exist: timestamp with time zone <= integer
LINE 1: ...om generate_series(1,100) where current_timestamp <= 2020-05...
^
HINT: No operator matches the given name and argument types. You might need to add explicit type casts.
QUERY: select 1 from generate_series(1,100) where current_timestamp <= 2020-05-25
CONTEXT: PL/pgSQL function foo(date) line 6 at RETURN QUERY
I got same error message. So there are more than one error:
The dynamic query is not valid - the where clause looks like
where current_timestamp <= 2020-05-25
and you can see, it is not valid - there are not quotes. You can fix it, when you use quotes manually (but it is strong error and don't do it, or you can use function quote_literal like where current_timestamp <= ' || quote_literal(d).
Now produced query is correct:
select 1 from generate_series(1,100) where current_timestamp <= '2020-05-25'
But in this case, is much better to use EXECUTE USING. When a variable is used as query parameter (not as table or column name), then you can use USING clause. Then you don't need to use quoting:
CREATE OR REPLACE FUNCTION public.foo(d date)
RETURNS TABLE(o integer)
LANGUAGE plpgsql
AS $function$
declare q text;
begin
q := 'select 1 from generate_series(1,100) where current_timestamp <= $1';
return query execute q using d;
end;
$function$
but the big mistake is using dynamic SQL when it is not necessary (like your example). There is not any visible reason why you use RETURN QUERY EXECUTE statement. You can use just RETURN QUERY:
CREATE OR REPLACE FUNCTION get_measurements_by_node_and_date(nodeID INTEGER, firstDate date, lastDate date)
RETURNS TABLE (measurement_id INTEGER, node_id INTEGER, carbon_dioxide DOUBLE PRECISION,
hydrocarbons DOUBLE PRECISION, temperature DOUBLE PRECISION,
humidity DOUBLE PRECISION,
air_pressure DOUBLE PRECISION,
measurement_timestamp timestamp without time zone ) AS
$$
BEGIN
RETURN QUERY SELECT measurements_lora.id,
measurements_lora.node_id,
measurements_lora.carbon_dioxide,
measurements_lora.hydrocarbons,
measurements_lora.temperature,
measurements_lora.humidity,
measurements_lora.air_pressure,
measurements_lora.measurement_timestamp AS measure
FROM public.measurements_lora
WHERE measurements_lora.measurement_timestamp <= lastDate
AND measurements_lora.measurement_timestamp >= firstDate
AND measurements_lora.node_id = nodeID;
END
$$ LANGUAGE plpgsql;

storing date in 'CCYYMMDD' format in Teradata

I would like to store dates in the format CCYYMMDD in Teradata, but I fail to do so. Find below what I tried so far:
query 1:
SEL CAST(CAST(CURRENT_DATE AS DATE FORMAT 'YYYYMMDD') AS VARCHAR(8))
-- Output: 20191230 ==> this works!
query 2:
SEL CAST(CAST(CURRENT_DATE AS DATE FORMAT 'CCYYMMDD') AS VARCHAR(8))
-- output: SELECT Failed. [3530] Invalid FORMAT string 'CCYYMMDD'.
It seems that the CCYYMMDD is not available in Teradata right away. Is there a workaround?
Tool used: Teradata SQL assistant
Internally, dates are stored as integers in Teradata. So when you say you want to store them in a different format, I don't think you can do that. But you can choose how to display / return the values.
I'm sure there's a cleaner way to get the format you want, but here's one way:
WITH cte (mydate) AS (
SELECT CAST(CAST(CURRENT_DATE AS DATE FORMAT 'YYYYMMDD') AS CHAR(8)) AS mydate
)
SELECT
CAST(
(CAST(SUBSTRING(mydate FROM 1 FOR 2) AS INTEGER) + 1) -- generate "century" value
AS CHAR(2) -- cast value as string
) || SUBSTRING(mydate FROM 3) AS new_date -- add remaining portion of date string
FROM cte
SQL Fiddle - Postgres
You'd have to add some extra logic to handle years before 1000 and after 9999. I don't have a TD system to test, but give it a try and let me know.

How to deal with a date stored as a string in an int field in Teradata?

I've got a table in Teradata that stores a date in an 8 character INT field in the following form "YYYYMMDD", so for today it would store "20180308". If I try to CAST it as a date like this:
CAST(date_field AS DATE FORMAT 'YYYY-MM-DD')
It transforms the date to some future date in the year 3450 or something.
I think it's an error that this data isn't either stored as a date object. Is there anyway to overcome this glitch? I don't have access to change this unfortunately.
Thanks
It's not an 8 character integer, it's an 8 digit integer.
Teradata stores dates using
(year - 1900) * 10000
+ (month * 100)
+ day
This results in 1180308 for today and 20180308 will return 3918-03-08
To cast it to a date you need to use
cast(intdate-19000000 as date)
select cast('20180308' as date format 'yyyymmdd') ;

Concatenating date and time fields

I have a table invoices with this fields:
invDate -> a date field
invTime -> a time field
I need to do querys like
SELECT top 10 * from invoices WHERE DATETIME(invDate+invTime)
BETWEEN DATETIME('2013-12-17 17:58') AND DATETIME()
or something like that. I don't know how to concatenate the invDate and invTime to create a datetime field. The only thing that i could do is this horribly thing:
DATETIME( YEAR(invDate), MONTH(invDate), DAY(invDate), 17, 52 ) AS MyDatetime
Couldn't even get hour and time with hour(invTime) and minute(invTime):
DATETIME( YEAR(invDate), MONTH(invDate), DAY(invDate),
HOUR(invTime), MINUTE(invTime) ) AS MyDatetime
I'm doing the querys throught the VFP Odbc Driver via PHP.
You were pretty close. If the value coming from PHP is not of a date/time, how could VFP interpret it properly. VFP also has a function CTOT() (character to time), and expects it in the format of 'yyyy-mm-ddThh:MM:ss??'
yyyy = 4 digit year
mm = 1 OR 2 digit month
dd = 1 OR 2 digit day
T -- literally the letter "T"
hh = 1 OR 2 digit hour (but typical is 2 anyhow)
MM = 1 or 2 digit minute (but typical is 2)
ss = 1 or 2 digit for seconds -- not required
?? = "AM" or "PM" if you wanted to explicitly provide that vs 24 hour clock
The MM and ss are optional, so if you finished with "T1" would be 1:00:00am
Now, to finish your query.
WHERE DATETIME(invDate+invTime)
BETWEEN DATETIME('2013-12-17 17:58') AND DATETIME()
Since this appears to be querying all invoices between a given date/time and NOW (via DateTime()), you don't even need between, you can do
WHERE YourTable.Column > CTOT( '2013-12-17T17:58')
If you specifically DID have a date/time range to consider, THEN you could do something like
WHERE YourTable.Column BETWEEN CTOT( '2013-12-05T10:00') AND CTOT( '2013-12-14T11:58')
PROBLEMS WITH your DATE() and TIME() implementations
The problem is Date() is a function to either return current date, or create based on y/m/d provided such as date( 2013, 12, 7 ). If you are passing a string, use CTOD( 'mm/dd/yyyy' ) such as CTOD( 12, 7, 2013 ).
As for the TIME() function that just expects a number and is of no use for you. From the OleDbProvider, your best bet is to just create a php function that builds a single string in the CTOT() format I've described and pass to the php function the date and time fields. Then use that as your "CTOT( functionReturnResult )"
To add a Date and a Time Field together you will need to convert them both to a same datatype 1st and than just simply add them together something like this....
DECLARE #D DATE = '2013-12-17'
DECLARE #T TIME = '17:58:00'
SELECT CAST(#D AS DATETIME) + CAST(#T AS DATETIME)
Result
2013-12-17 17:58:00.000
Your Query
SELECT top 10 *
from invoices
WHERE CAST(invDate AS DATETIME) + CAST(invTime AS DATETIME)
BETWEEN '20131217 17:58:00.000' AND GETDATE()