I am trying to write below UDF in snowflake.But it doesnt allow to use sql functions like TO_DATE,DATE_ADD,dayofweek . Any alternative idea would be helpful. Thanks.
CREATE OR REPLACE FUNCTION getShippingDate(deliveryDate varchar,deliveryCountryCode varchar, holidayList varchar,deliveryDays varchar)
RETURNS VARCHAR
LANGUAGE JAVASCRIPT
AS $$
deliveryPeriod = 0
weekDay = 0
deliveryDate= TO_DATE(deliveryDate)
if(deliveryCountryCode != 'IN') {
deliveryPeriod = 2
}
else {
deliveryPeriod = deliveryDays + 1
}
if(deliveryPeriod <= 0) {
deliveryPeriod = 1
}
/* substract delivery period from delivery date */
deliveryDate = DATEADD(Day ,-deliveryPeriod, to_date(deliveryDate))
weekDay = dayofweek(deliveryDate)
/* if shipping date falls on sunday then substract 2 days */
if (weekDay == 0) {
deliveryDate = DATEADD(Day ,-2, to_date(deliveryDate))
}
/* if shipping date falls on saturday then substract 1 days */
if(weekDay == 6){
deliveryDate = DATEADD(Day ,-1, to_date(deliveryDate))
}
/* check if shipping date falls on holiday then substract 1 */
if(charindex(deliveryDate , holidayList) > 0) {
deliveryDate = DATEADD(Day ,-1, to_date(deliveryDate))
}
return deliveryDate
$$```
It is relatively easy to create date functions in JavaScript. Look at example below.
Just remember:
Please use the DATE SQL datatype for input and output
Parameters must be "Quoted" if you want to reference them in MixedCase
Use a semicolon ; to terminate each statement
CREATE OR REPLACE FUNCTION getShippingDate("deliveryDate" DATE, "offset" FLOAT)
RETURNS DATE
LANGUAGE JAVASCRIPT
AS $$
function day_add(dt, days) {
return new Date(dt.getFullYear(), dt.getMonth(), dt.getDate() + offset);
}
return day_add(deliveryDate, offset);
$$;
SELECT getShippingDate(CURRENT_DATE, -2);
new Date() does some magic when you add or subtract outside the days of the month.
Implementing your function as a scalar SQL UDF would solve the problem in this case. Or you could implement your own javascript date math. For example. The good news is that you can call JS UDFs from your SQL UDFs.
You may need to try like below:
CREATE OR REPLACE PROCEDURE dbo.usp_test()
returns VARCHAR
LANGUAGE javascript
EXECUTE AS CALLER
AS
$$
snowflake.createStatement( {sqlText: "SET dt = DATEADD(MONTH, 1,CURRENT_TIMESTAMP)"} ).execute();
return 'success';
$$
;
-- CALL ds_work.ddd.usp_test()
Related
I have a snippet of a table as below :
Can somebody help me with the query that gives me report as below:
If you don't want to use a calendar dimension as Mike suggested (which is a good idea) the best option may be a user defined table function (UDTF). This UDTF does not depend on the DAYS_COUNT column. It calculates the days in each month and only requires a start and end date. It will generate as many rows as are required to fill the months in between.
select * from a, table(DAYS_IN_MONTHS(START_DATE, END_DATE));
create or replace function DAYS_IN_MONTHS(START_DATE date, END_DATE date)
returns table(MONTHS string, DAYS float)
language javascript
strict immutable
as
$$
{
initialize: function (argumentInfo, context) {},
processRow: function (row, rowWriter, context) {
let year = row.START_DATE.getFullYear();
let month = row.START_DATE.getMonth();
let endYear = row.END_DATE.getFullYear();
let endMonth = row.END_DATE.getMonth();
let isDone = year > endYear || (year == endYear && month > endMonth);
if (year == endYear && month == endMonth) {
rowWriter.writeRow({MONTHS: `${year}-${(""+(month+1)).padStart(2, '0')}`,DAYS: row.END_DATE.getDate() - row.START_DATE.getDate()});
isDone = true;
}
d = row.START_DATE
while (!isDone) {
if (year == endYear && month == endMonth) {
rowWriter.writeRow({MONTHS: `${year}-${(""+(month+1)).padStart(2, '0')}`,DAYS: row.END_DATE.getDate() - (d.getDate() - 1) });
isDone = true;
} else {
rowWriter.writeRow({MONTHS: `${year}-${(""+(month+1)).padStart(2, '0')}`,DAYS: new Date(year, month + 1, 0).getDate() - (d.getDate() - 1) });
month++;
if (month == 12) {
month = 0;
year++;
}
}
d = new Date(year, month, 1);
}
},
finalize: function (rowWriter, context) {},
}
$$;
It would be very helpful if you'd have a calendar dimension for something like this. As an example, you can create one with something like this:
CREATE TEMP TABLE calendar AS
SELECT dateadd(day,seq4(),'2017-01-01'::date) as cal_date,
date_trunc(month,cal_date) as cal_month_start,
dateadd(day,-1,(dateadd(month,1, cal_month_start))) as cal_month_end
FROM table(generator(rowcount => 10000));
With this calendar table, you can then join to it using the start and end dates and aggregate on that date to get the results. Using a CTE to replicate your data:
WITH x AS (
SELECT member_id, start_date, end_date
FROM (VALUES ('461043068_02','2018-08-07'::date,'2018-08-17'::date),
('461043068_01','2019-05-28'::date,'2019-06-28'::date)
) y (member_id, start_date, end_date)
)
Now, you can query x and join to calendar as such:
SELECT member_id, cal_month_start, count(*)
FROM x
JOIN calendar c
ON c.cal_date between dateadd(day,1,x.start_date) and x.end_date
GROUP BY 1,2;
This gives you the results you are looking for. Please note the need to add 1 to the start_date, so that you don't count the "edges" of your date ranges twice.
Also, I didn't format the cal_month_start in my query, but you can do that using a TO_VARCHAR() function, if needed.
I am trying to create a function that change the day of a football league; if the match is fixed on saturday then the function update the match day to be the previuos friday, and if the match is fixed on sunday the function update the match date to be on monday. Also the function will show how many rows have been update.
The table I use is as follow:
CREATE TABLE "183400_Matches_Details" (
"183400_Stadiums_id" INTEGER NOT NULL,
"183400_Teams_id" INTEGER NOT NULL,
"183400_Teams_id1" INTEGER NOT NULL,
"183400:Referees_id" INTEGER NOT NULL,
"183400_Matches_number" INTEGER NOT NULL,
"date" DATE NOT NULL,
result VARCHAR2(5) NOT NULL
);
I tried the following statements to build the function, but it always gives me an error:
create or replace function updateDay (
v_number "183400_Matches_Details"."183400_Matches_number"%type)
return date
as
v_fecha "183400_Matches_Details"."date"%type;
begin
SELECT TO_CHAR("date", 'DAY', 'NLS_DATE_LANGUAGE=ENGLISH') as day1 into v_fecha FROM
"183400_Matches_Details"
where "183400_Matches_number" = v_number;
if day1 = 'SATURDAY' then
update "183400_Matches_Details"
set "date" = "date"-1
where "183400_Matches_number" = v_number;
elsif day1 = 'SUNDAY' then
update "183400_Matches_Details"
set "date" = "date"+1
where "183400_Matches_number" = v_number;
end if;
return SQL%ROWCOUNT;
end;
/
select * from "183400_Matches_Details"
DECLARE
v_number "183400_Matches_Details"."183400_Matches_number"%type := &number;
v_total_filas number(8);
BEGIN
v_total_filas := actualizaPrecioCoche(v_number);
DBMS_OUTPUT.put_line('There are ' || v_total_filas || ' updated rows');
END;
/
Any ideas to make it run correctly?=)
I changed your function as it should be. Try below.
CREATE TABLE "183400_Matches_Details"
(
"183400_Stadiums_id" INTEGER NOT NULL,
"183400_Teams_id" INTEGER NOT NULL,
"183400_Teams_id1" INTEGER NOT NULL,
"183400:Referees_id" INTEGER NOT NULL,
"183400_Matches_number" INTEGER NOT NULL,
"datee" DATE NOT NULL,
RESULT VARCHAR2 (5) NOT NULL
);
CREATE OR REPLACE FUNCTION updateDay (
v_number "183400_Matches_Details"."183400_Matches_number"%TYPE)
RETURN DATE
AS
v_fecha "183400_Matches_Details"."datee"%TYPE;
sql_qry VARCHAR2 (400 CHAR);
BEGIN
sql_qry :=
'SELECT TO_CHAR(datee, ''DAY'', ''NLS_DATE_LANGUAGE=ENGLISH'') where "183400_Matches_number"='
|| v_number;
EXECUTE IMMEDIATE sql_qry INTO v_fecha;
IF v_fecha = 'SATURDAY'
THEN
UPDATE "183400_Matches_Details"
SET "datee" = "datee" - 1
WHERE "183400_Matches_number" = v_number;
ELSIF v_fecha = 'SUNDAY'
THEN
UPDATE "183400_Matches_Details"
SET "datee" = "datee" + 1
WHERE "183400_Matches_number" = v_number;
END IF;
RETURN to_date('19000101','yyyymmdd') ;
END;
/
After a second look I realized what your asking is actually quite simple: Given a date that is Sat update it to Fri, and that is Sun update to Mon. This can actually be done in a single SQL statement.
I changed it from a function to a procedure as the purpose is to Update the database, and return the number of rows processed. But as a function it makes the purpose to get the row count and updating the database as a side effect. Names and types (IMHO) should always reflect the purpose of the routine. I did 'return' the row count as an OUT parameter - it being an informational side effect. See fiddle for full example.
create or replace
procedure reschedule_sat_sun_match_details(
p_match_number in "183400_Matches_Details"."183400_Matches_number"%type
, p_rows_updated out number)
as
begin
update "183400_Matches_Details"
set "date" = case to_char("date", 'dy')
when 'sat' then "date"-1 -- Sat update to Fri
when 'sun' then "date"+1 -- Sun update to Mon
end
where to_char("date", 'dy') in ('sat','sun')
and "183400_Matches_number" = p_match_number;
p_rows_updated := sql%rowcount;
end reschedule_sat_sun_match_details;
For day of week values I used the format 'dy' rather than 'day'. The difference being 'dy' returns day name abbreviations with a constant length without padding, while 'day' pads the returned values to the length of the longest day name (to get constant length) thus "sunday" is returned as "sunday " to match the length of "wednesday".
A couple other suggestions. Avoid Mixed Case name and names beginning with numbers. These require double quoting (") on every reference. This becomes a pain to just write and your queries much harder to read and understand. a table name Matches_Details_183400 the exact same information without requiring the quotes. (Yes Oracle will make it upper case in messages it it issues but you can still write it in mixed case if you wish - it will still be the same name.) It gives you no benefit but a lot of pain.
As #hotfix mentioned do not use reserved or keywords as object names. Oracle has documented such words and reserves the right to enforce a specific meaning whenever they choose. If/When they do makes an almost untraceable bug to find.
(Submitting on behalf of a Snowflake User...)
QUESTION:
Is it possible to nest multiple functions inside of a function and pass all the parameters required?
for example...
CREATE OR REPLACE FUNCTION "udf_InteractionIndicator"("ROH_RENEWAL_SYSTEM_STATUS1" VARCHAR(100), "GOLS_OPPORTUNITY_LINE_STATUS" VARCHAR(100)
, "ROH_CLIENT_CURRENT_TEMPERATURE1" VARCHAR(100)
, "ROH_PO_ATTACHED" VARCHAR(100)
, "ROH_PO_NUMBER" VARCHAR(100)
, "RT_PAID_OVERRIDE" VARCHAR(100), "ROH_RENEWAL_OPPORTUNITY_STATUS1" VARCHAR(100)
, "ROH_RENEWAL_CONVERSATION_DATE" DATE, "ROH_APPROVAL_RECEIVED_DATE" DATETIME)
RETURNS NUMBER(1,0)
AS
$$
CASE WHEN ("udf_RenewalNoticeSentIndicator"("ROH_RENEWAL_SYSTEM_STATUS1", "ROH_CLIENT_CURRENT_TEMPERATURE1"
, "GOLS_OPPORTUNITY_LINE_STATUS"
, "ROH_PO_ATTACHED", "RT_PAID_OVERRIDE"
, "ROH_RENEWAL_OPPORTUNITY_STATUS1")) = 1
AND (ROH_RENEWAL_CONVERSATION_DATE IS NOT NULL
OR ("udf_AuthorizedIndicator"(ROH_APPROVAL_RECEIVED_DATE, "ROH_PO_ATTACHED", "ROH_PO_NUMBER")) = 1
OR ("udf_PaidIndicator"("GOLS_OPPORTUNITY_LINE_STATUS")) = 1
OR ("udf_ChurnIndicator"("GOLS_OPPORTUNITY_LINE_STATUS")) = 1
)
THEN 1 ELSE 0 END
$$
;
I've received the recommendation to:
...create a SQL UDF or JavaScript UDF. A JavaScript UDF can only
contain JavaScript code, and an SQL UDF can contain only one SQL
statement (no DML and DDL). In case of nesting, SQL UDF can call
another SQL UDF or a JavaScript UDF but the same is not true with the
JavaScript UDF(it only contains JavaScript code).
CREATE OR REPLACE FUNCTION udf_InteractionIndicator_nested(ID DOUBLE)
RETURNS DOUBLE
AS
$$
SELECT ID
$$;
create or replace function js_factorial(d double)
returns double
language javascript
strict
as '
if (D <= 0) {
return 1;
} else {
var result = 1;
for (var i = 2; i <= D; i++) {
result = result * i;
}
return result;
}
';
CREATE OR REPLACE FUNCTION udf_InteractionIndicator(ID DOUBLE)
RETURNS double
AS
$$
select udf_InteractionIndicator_nested(ID) + js_factorial(ID)
$$;
select udf_InteractionIndicator(4);
+-----------------------------+
| UDF_INTERACTIONINDICATOR(4) |
|-----------------------------|
| 28 |
+-----------------------------+
HOWEVER, I'm trying to accomplish this with a SQL UDF. It makes sense that a nested function can be created as long as they use the same parameter. I'd like to create a function that accepts say 8 parameters and the underlying functions may reference all, some or none of the parent function parameters. That is where I run into an issue... THOUGHTS?
(A consultant in our community offered the following answer...)
With a JavaScript UDF the design will be much more compact and maintainable, if your use case is that there is a "main" function that breaks down work into subfunctions which will only be invoked from main.
Then you simply define all underlying functions within the main function, which is possible with JavaScript but not with an SQL UDF, and then you are free to use the main parameters everywhere.
CREATE OR REPLACE FUNCTION MAIN_JS(P1 STRING, P2 FLOAT)
RETURNS FLOAT
LANGUAGE JAVASCRIPT
AS '
function helper_1(p) { return p * 2; }
function helper_2(p) { return p == "triple" ? P2 * 3 : P2; }
return helper_1(P2) + helper_2(P1);
';
SELECT MAIN_JS('triple', 4); -- => 20
I am writing a stored procedure that takes in 4 parameters: confirmation_number, payment_amount, start_range, end_range.
The parameters are optional, so I am doing a check in this fashion for the confirmation_number, and the payment_amount parameters:
IF (#s_Confirmation_Number IS NOT NULL)
SET #SQL = #SQL + ' AND pd.TransactionNumber = #s_Confirmation_Number'
IF (#d_Payment_Amount IS NOT NULL)
SET #SQL = #SQL + ' AND pd.PaymentAmount = #d_Payment_Amount'
I would like to ask for help because I am not sure what is the best method to check for the date range parameters.
If someone could give me en example, or several on how this is best achieved it would be great.
Thank you in advance.
UPDATE - after receiving some great help -.
This is what I have so far, I am following scsimon recommendation, but I am not sure about the dates, I got the idea from another post I found and some playing around with it. Would you care looking at it and tell me what you all think?
Many thanks.
#s_Confirmation_Number NVARCHAR(50) = NULL
, #d_Payment_Amount DECIMAL(18, 2) = NULL
, #d_Start_Range DATE = NULL
, #d_End_Range DATE = NULL
...
....
WHERE
ph.SourceType = #s_Source_Type
AND ((pd.TransConfirmID = #s_Confirmation_Number) OR #s_Confirmation_Number IS NULL)
AND ((pd.PaymentAmount = #d_Payment_Amount) OR #d_Payment_Amount IS NULL)
AND (((NULLIF(#d_Start_Range, '') IS NULL) OR CAST(pd.CreatedDate AS DATE) >= #d_Start_Range)
AND ((NULLIF(#d_End_Range, '') IS NULL) OR CAST(pd.CreatedDate AS DATE) <= #d_End_Range))
(The parameter sourceType is a hard-coded value)
This is called a catch all or kitchen sink query. It is usually written as such:
create procedure myProc
(#Payment_Amount int = null
,#Confirmation_Number = null
,#start_range datetime
,#end_range datetime)
as
select ...
from ...
where
(pd.TransactionNumber = #Confirmation_Number or #Confirmation_Number is null)
and (pd.PaymentAmount = #Payment_Amount or #Payment_Amount is null)
The NULL on the two parameters gives them a default of NULL and makes them "optional". The WHERE clause evaluates this to only return rows where your user input matches the column value, or all rows when no user input was supplied (i.e. parameter IS NULL). You can use this with the date parameters as well. Just pay close attention to your parentheses. They matter a lot here because we are mixing and and or logic.
Aaron Bertrand has blogged extensively on this.
I do it like this
WHERE
COALESCE(#s_Confirmation_Number,pd.TransactionNumber) = pd.TransactionNumber AND
COALESCE(#d_Payment_Amount,pd.PaymentAmount) = pd.PaymentAmount
If we have a value for each of these parameters then it will check against the filter value otherwise it will always match the filter value if the parameter is null.
I've found that using COALESCE is faster and clearer than IF control statements or using OR in the WHERE clause.
There is another way.
But I tested and realized that a scsimon query is faster than mine.
AND (CASE
WHEN #Confirmation_Number is not null
THEN (CASE
WHEN pd.TransactionNumber = #Confirmation_Number
THEN 1
ELSE 0
END)
ELSE 1
END = 1)
I am trying to convert a a varchar array into a date array. Currently the date is being accessed from a Java class in the form of a String but it needs to be a date there. Therefore I need to convert the varchar array into a date array. I can only see questions here dealing with converting a string to a date format, which is not what I want. How different is it to convert a varchar array into a date array?
this is the variable which I need the procedure to convert.
P_IN_GRID_EFFECTIVE_DATE IN P_DVP_CONSTS.T_VCHAR10ARRAY,
You can loop around one array and use its contents to populate a second one; in this case with a to_date() call for each entry. As a simple demo:
create procedure convert_array(
P_IN_GRID_EFFECTIVE_DATE IN P_DVP_CONSTS.T_VCHAR10ARRAY,
P_OUT_GRID_EFFECTIVE_DATE OUT P_DVP_CONSTS.T_DATEARRAY
) as
begin
P_OUT_GRID_EFFECTIVE_DATE := new P_DVP_CONSTS.T_DATEARRAY();
P_OUT_GRID_EFFECTIVE_DATE.extend(P_IN_GRID_EFFECTIVE_DATE.count);
for i in 1..P_IN_GRID_EFFECTIVE_DATE.count loop
P_OUT_GRID_EFFECTIVE_DATE(i) :=
to_date(P_IN_GRID_EFFECTIVE_DATE(i), 'YYYY-MM-DD');
-- or whatever format your strings are using
end loop;
end convert_array;
/
And a quick test:
set serveroutput on
declare
l_strings P_DVP_CONSTS.T_VCHAR10ARRAY;
l_dates P_DVP_CONSTS.T_DATEARRAY;
begin
l_strings := P_DVP_CONSTS.T_VCHAR10ARRAY('2015-06-08', '2015-07-09',
'2015-08-10');
convert_array(l_strings, l_dates);
dbms_output.put_line('Number of dates in array: ' || l_dates.count);
for i in 1..l_dates.count loop
dbms_output.put_line('Date ' || i
|| ': ' || to_char(l_dates(i), 'DD/MM/YYYY'));
end loop;
end;
/
PL/SQL procedure successfully completed.
Number of dates in array: 3
Date 1: 08/06/2015
Date 2: 09/07/2015
Date 3: 10/08/2015
Types and function:
create type tv as varray(5) of varchar2(15);
create type td as varray(5) of date;
create or replace function v2d (i_varchars in tv)
return td pipelined is
begin
for i in 1..i_varchars.count
loop
pipe row (to_date(i_varchars(i), 'mm/dd/yyyy'));
end loop;
end;
Test:
select * from table(v2d(tv('09/01/2010', '06/15/2015')));
COLUMN_VALUE
------------
2010-01-01
2015-06-15