Returning tabular data from Snowflake SQL procedure - sql

I'm trying to write a stored procedure that will return the results of a complex query:
CREATE OR REPLACE PROCEDURE sp_partition_ohlc(symbol VARCHAR(10),
from_time NUMERIC(20, 0),
til_time NUMERIC(20, 0),
step_time NUMERIC(20, 0))
RETURNS TABLE()
LANGUAGE SQL
AS
$$
BEGIN
SELECT
:step_time * bucket AS t,
SUM(T.volume) AS v,
MAX(T.open) AS o,
MAX(T.close) AS c,
MAX(T.highlow) AS h,
MIN(T.highlow) AS l,
v * (h + l + c) / 3 AS wv,
COUNT(*) AS n
FROM (
SELECT
FLOOR(T.sip_timestamp / :step_time) AS bucket,
cta_calc(T.size, T.conditions, 'VOLUME') AS volume,
cta_calc(T.price, T.conditions, 'HIGHLOW') AS highlow,
IFF(ROW_NUMBER() OVER (PARTITION BY bucket ORDER BY T.sip_timestamp) = 1, T.price, NULL) AS open,
LAST_VALUE(cta_calc(T.price, T.conditions, 'LAST'))
IGNORE NULLS OVER (PARTITION BY bucket ORDER BY T.sip_timestamp) AS close
FROM trades AS T
WHERE
T.symbol = :symbol AND
T.sip_timestamp >= :from_time AND
T.sip_timestamp < :til_time) AS T
GROUP BY bucket
ORDER BY bucket;
END
$$
This compiles properly but, if I try to run it:
CALL sp_partition_ohlc('NTAP', 1640995200000000000, 1672531200000000000, 3600000000000)
I get the following error:
092222 (P0000): SQL compilation error: stored procedure is missing a return statement
From this, I understand that I should have a RETURN in front of my outer SELECT statement. However, when I modify the procedure thus, it fails to compile with this error:
Syntax error: unexpected 'SELECT'. (line 7)
syntax error line 4 at position 23 unexpected '*'. (line 7)
I'm not sure what this error is telling me as, when I run the query by itself, it returns results. I assume I'm doing something wrong in setting up the procedure but I'm not sure what it is from the associated documentation, here. Any help would be appreciated.

It is not a matter of just putting a return in there:
you more want to follow a pattern like:
create or replace procedure get_top_sales()
returns table (sales_date date, quantity number)
language sql
as
declare
res resultset default (select sales_date, quantity from sales order by quantity desc limit 10);
begin
return table(res);
end;
here the res is a declared as the result set, which is the SQL to run, so you can access those, and then it is returned as a table via table(res) and the type of the res matches the return type of the function returns table (sales_date date, quantity number), so for your function you would need to alter this also.
so something like (I have not run):
CREATE OR REPLACE PROCEDURE sp_partition_ohlc(symbol VARCHAR(10),
from_time NUMERIC(20, 0),
til_time NUMERIC(20, 0),
step_time NUMERIC(20, 0))
RETURNS TABLE(t int, v float, o float, c float, h float, l float, wv float, n int) /* guessed types */
LANGUAGE SQL
AS
$$
DECLARE
res resultset;
BEGIN
let res := (
SELECT
:step_time * bucket AS t,
SUM(T.volume) AS v,
MAX(T.open) AS o,
MAX(T.close) AS c,
MAX(T.highlow) AS h,
MIN(T.highlow) AS l,
v * (h + l + c) / 3 AS wv,
COUNT(*) AS n
FROM (
SELECT
FLOOR(T.sip_timestamp / :step_time) AS bucket,
cta_calc(T.size, T.conditions, 'VOLUME') AS volume,
cta_calc(T.price, T.conditions, 'HIGHLOW') AS highlow,
IFF(ROW_NUMBER() OVER (PARTITION BY bucket ORDER BY T.sip_timestamp) = 1, T.price, NULL) AS open,
LAST_VALUE(cta_calc(T.price, T.conditions, 'LAST'))
IGNORE NULLS OVER (PARTITION BY bucket ORDER BY T.sip_timestamp) AS close
FROM trades AS T
WHERE
T.symbol = :symbol AND
T.sip_timestamp >= :from_time AND
T.sip_timestamp < :til_time) AS T
GROUP BY bucket
ORDER BY bucket
);
return table(res);
END
$$

Related

Snowflake SQL UDF - Unsupported Subquery Error

I am creating a Snowflake SQL UDF. I keep running into SQL compilation error: Unsupported subquery type cannot be evaluated. I have tried to do several things to go around the issue, this being my latest try.
How can I make this break out of the subquery'ing error?
The UDF should allow one to input their preferred year. Thinking to create a solution by if a year is not provided, the default would be the present year.
create or replace function new_value(PRICE float, TYPE varchar, YR_CREATED int, YEAR int)
returns float
as
$$
with AGE_OF_PRODUCT as (
select any_value((YEAR - YR_CREATED)) as AGE ),
FORMULA as (
select any_value(AGE) as AGE,
any_value(case
when AGE <= 1 then 1
else 2
end) as FUNCTION
from AGE_OF_PRODUCT
)
select
any_value(case
when F.FUNCTION = 1 then (PRICE - (PRICE * R.R1))
else (PRICE * (1 - (R.R1))) * pow((1-(R.R2)), ((F.AGE - YR_CREATED)-1))
end) as VALUE
from FORMULA as F, RATES as R
where TYPE = R.TYPE_OF_PRODUCT
$$;
So the main problem is you are likely using the function like:
select v.*,
new_value(v.price, v.type, v.yr_create, v.year) as awesome
from table_with_values as v
also your UDF can be rewritten as it stands as:
create or replace function new_value(
PRICE float,
TYPE varchar,
YR_CREATED int,
YEAR int)
returns float
as
$$
select
YEAR - YR_CREATED as age,
case age <= 1
when true then (PRICE - (PRICE * r.r1))
else (PRICE * (1 - (r.r1))) * pow((1-(r.r2)), ((age - YR_CREATED)-1))
end as value
from rates as r
where TYPE = r.type_of_product
$$;
but if we move the join to rates outside the UDF
create or replace function new_value(
PRICE float,
YR_CREATED int,
YEAR int,
rate1 float,
rate2 float)
returns float
as
$$
select
case (YEAR - YR_CREATED) <= 1
when true then (PRICE - (PRICE * r.r1))
else (PRICE * (1 - (rate1))) * pow((1-(rate2)), (((YEAR - YR_CREATED) - YR_CREATED)-1))
end as value;
$$;
then we can call it like:
select v.*,
new_value(v.price, v.yr_create, v.year, r.r1, r.r2) as awesome
from table_with_values as v
join rates as r
on v.type = r.type_of_product

How can I write SQL select statement inside scalar type user-defined function?

I am trying to create a function in SQL Server using the following, but I think I am missing something in either in syntax or query
CREATE FUNCTION DEMO.Get_Rate_For_Absence
(#company_id_ VARCHAR,
#emp_no_ VARCHAR,
#account_date_ DATE)
RETURN DECIMAL(10, 2) AS
BEGIN
DECLARE #RATE_ DECIMAL(10, 2)
SET #RATE_ = SELECT rate
FROM DEMO.Employee
WHERE COMPANY_ID = '#company_id_ '
AND Emp_no = '#emp_no_ '
AND ORG_CODE = '#wage_code_'
AND ACCOUNT_DATE = '#account_date_'
RETURN #RATE
END
The SQL statement that I am trying to write inside function code block is:
SELECT DISTINCT rate
FROM DEMO.Employee
WHERE Company_ID = #company_id_
AND EMP_NO = #emp_no_
AND ACCOUNT_DATE = #account_date_
Something like:
CREATE OR ALTER FUNCTION DEMO.Get_Rate_For_Absence
(#company_id VARCHAR(200),
#emp_no VARCHAR(200),
#account_date DATE)
RETURNS DECIMAL(10, 2) AS
BEGIN
DECLARE #RATE DECIMAL(10, 2)
SET #RATE = (
SELECT rate
FROM DEMO.Employee
WHERE COMPANY_ID = #company_id
AND Emp_no = #emp_no
AND ACCOUNT_DATE = #account_date
)
RETURN #RATE
END
Perhaps you actually want to return a whole resultset rather than just a single value.
Then you should use an inline Table Valued Function (of the form RETURNS TABLE AS RETURN SELECT ...) which in any case performs much better than a scalar function.
Variables don't go in quotes so you just do COMPANY_ID = #company_id_.
Always declare varchar with a length.
CREATE OR ALTER FUNCTION DEMO.Get_Rate_For_Absence (
#company_id_ VARCHAR(100),
#emp_no_ VARCHAR(100),
#wage_code_ VARCAHR(100),
#account_date_ DATE
)
RETURNS TABLE AS RETURN
SELECT e.rate
FROM DEMO.Employee e
WHERE e.COMPANY_ID = #company_id_
AND e.Emp_no = #emp_no_
AND e.ORG_CODE = #wage_code_
AND e.ACCOUNT_DATE = #account_date_;
You use it slightly differently than scalar functions, as it goes in the FROM part
SELECT r.rate
FROM DEMO.Get_Rate_For_Absence('a', 'b', 'c', GETDATE()) r;
Or
SELECT r.rate
FROM SomeTable t
CROSS APPLY DEMO.Get_Rate_For_Absence(t.a, t.b, t.c, t.date) r;

Setting variable in a Postgres function

CREATE OR REPLACE FUNCTION "freeTicket" (eid integer NOT NULL)
DECLARE
couponCode text
BEGIN
INSERT INTO purchases p (cid, pdate, eid, ccode)
VALUES
(
SELECT p.cid, GETDATE(), $1, couponCode FROM purchase p
GROUP BY p.cid
HAVING COUNT(1) > 5
ORDER BY p.cid
);
END; LANGUAGE plpgsql;
I need to set the variable of couponCode to the output of:
Select code from couponCode where eid = $1 and percentage = 100;
And use it in the insert query above.
What is the best way to do this?
That would be SELECT <expressions> INTO <variables> FROM ..., but you can do it all in one statement:
INSERT INTO purchases p (cid, pdate, eid, ccode)
SELECT p.cid,
current_date,
$1,
(SELECT code FROM couponcode
WHERE eid = $1 AND percentage = 100)
FROM purchase p
GROUP BY p.cid
HAVING COUNT(1) > 5:
ORDER BY makes no sense here.
Basics about assigning variables in PL/pgSQL:
Store query result in a variable using in PL/pgSQL
Apart from that, your function has a number of syntax errors and other problems. Starting with:
CREATE OR REPLACE FUNCTION "freeTicket" (eid integer NOT NULL)
DECLARE ...
NOT NULL isn't valid syntax here.
You must declare the return type somehow. If the function does not return anything, add RETURNS void.
For your own good, avoid CaMeL-case identifiers in Postgres. Use legal, lower-case identifiers exclusively if possible. See:
Are PostgreSQL column names case-sensitive?
The function would work like this:
CREATE OR REPLACE FUNCTION free_ticket(_eid integer, OUT _row_ct int) AS
$func$
DECLARE
coupon_code text; -- semicolon required
BEGIN
INSERT INTO purchases (cid, pdate, eid, ccode)
SELECT cid, now()::date, _eid
, (SELECT code FROM couponCode WHERE eid = _eid AND percentage = 100)
FROM purchase
GROUP BY cid
HAVING COUNT(*) > 5 -- count(*) is faster
ORDER BY cid; -- ORDER BY is *not* pointless.
GET DIAGNOSTICS _row_ct := ROW_COUNT;
END
$func$ LANGUAGE plpgsql;
The added OUT row_ct int is returned at the end of the function automatically. It obviates the need for an explicit RETURNS declaration.
You also had a table alias in:
INSERT INTO purchases p (cid, pdate, eid, ccode)
But INSERT statements require the AS keyword for aliases to avoid ambiguity (unlike other DML statements). So: INSERT INTO purchases AS p .... But no need for an alias since there is no ambiguity in the statement.
Related:
Count the rows affected by plpgsql function
Asides: Two tables named purchase and purchases, that's bound to lead to confusion. And the second table might also be replaced with a VIEW or MATERIALIZED VIEW.

Create function return integer SQL Server 2008

I was trying to create a function which returns to an integer. However, I got the warning as
"Msg 2715, Level 16, State 3, Procedure median, Line 1
Column, parameter, or variable #0: Cannot find data type Median."
Here is the query. Thanks in advance.
CREATE FUNCTION dbo.median (#score int)
RETURNS Median
AS
BEGIN
DECLARE #MedianScore as Median;
SELECT #MedianScore=
(
(SELECT MAX(#score) FROM
(SELECT TOP 50 PERCENT Score FROM t ORDER BY Score) AS BottomHalf)
+
(SELECT MIN(#score) FROM
(SELECT TOP 50 PERCENT Score FROM t ORDER BY Score DESC) AS TopHalf)
) / 2 ;
RETURN #MedianScore;
END;
GO
Just change the return type to integer:
CREATE FUNCTION dbo.median (#score int)
RETURNS integer
AS
BEGIN
DECLARE #MedianScore as integer;
Unless you're intentionally using the Median type for something that you haven't stated.
Since you are calculating Median of some values I would suggest you return a Numeric value instead of Integer as MAX(#score) + MIN(#score)/ 2 can return a decimal number value. so trying to save that value in an INT variable will truncate the Decimal part. which can lead to wrong results.
In the following example I have used NUMERIC(20,2) return value.
CREATE FUNCTION dbo.median (#score int)
RETURNS NUMERIC(20,2)
AS
BEGIN
DECLARE #MedianScore as NUMERIC(20,2);
SELECT #MedianScore=
(
(SELECT MAX(#score) FROM
(SELECT TOP 50 PERCENT Score FROM t ORDER BY Score) AS BottomHalf)
+
(SELECT MIN(#score) FROM
(SELECT TOP 50 PERCENT Score FROM t ORDER BY Score DESC) AS TopHalf)
) / 2 ;
RETURN #MedianScore;
END;
GO
or if you do want to return an INTEGER use round function inside the function something like this..
CREATE FUNCTION dbo.median (#score int)
RETURNS INT
AS
BEGIN
DECLARE #MedianScore as INT;
SELECT #MedianScore=ROUND(
(
(SELECT MAX(#score) FROM
(SELECT TOP 50 PERCENT Score FROM t ORDER BY Score) AS BottomHalf)
+
(SELECT MIN(#score) FROM
(SELECT TOP 50 PERCENT Score FROM t ORDER BY Score DESC) AS TopHalf)
) / 2, 0) ;
RETURN #MedianScore;
END;
GO
You must declare a datatype on RETURNS. "Median" is not a type.
CREATE FUNCTION dbo.median (#score int)
RETURNS real -- you can use also float(24), numeric(8,3), decimal(8,3)...
AS
BEGIN
DECLARE #MedianScore as real;
SELECT #MedianScore=
(
(SELECT MAX(#score) FROM
(SELECT TOP 50 PERCENT Score FROM t ORDER BY Score) AS BottomHalf)
+
(SELECT MIN(#score) FROM
(SELECT TOP 50 PERCENT Score FROM t ORDER BY Score DESC) AS TopHalf)
) / 2 ;
RETURN #MedianScore;
END;
GO
create function [dbo].[Sum]
(
#x int,
#y int
)
RETURNS int
AS
BEGIN
return #x+#y
END

Converting a script from MSSQL to PL/pgSQL

I just started working with the EVE static dump, which is just a lot of tables with data about the game, such as a list of what solar systems connect, which is what I'm dealing with.
I want to make a webpage that lets you filter out systems, and the first step is getting a list of systems nearby, with the distance to them.
I found a script that does it for MSSQL
--By Joanna Davaham http://forum.eveuniversity.org/viewtopic.php?t=44601&p=396107#p424943
--set values
DECLARE #jumpsAway INT =10
DECLARE #MiddleSystemName VARCHAR(50) = 'Aldrat'
DECLARE #Level INT =1
IF OBJECT_ID('tempdb..#map') IS NOT NULL
DROP TABLE #map
CREATE TABLE #map
(fromSolarSystemID INT, toSolarSystemID INT, Level INT)
INSERT INTO #map
SELECT -1, mSS.solarSystemID, 0 FROM mapSolarSystems mSS
WHERE mSS.solarSystemName= #MiddleSystemName
WHILE #Level <= #jumpsAway
BEGIN
INSERT INTO #map
SELECT mSSJ.fromSolarSystemID, mSSJ.toSolarSystemID, #Level FROM mapSolarSystemJumps mSSJ
WHERE mSSJ.fromSolarSystemID IN (SELECT toSolarSystemID FROM #map WHERE Level = #Level-1)
AND mSSJ.fromSolarSystemID NOT IN (SELECT fromSolarSystemID FROM #map)
SET #Level=#Level+1
END
SELECT m.*, mSS.solarSystemName, mSS.security FROM #map m
JOIN mapSolarSystems mSS ON m.toSolarSystemID=mSS.solarSystemID
--WHERE mSS.security<0.45 --uncomment to check all nearby lowsec system
I know that I could probably just use the MSSQL version of the dump, but I also want to be learning more about how to use PostgreSQL better.
I understand what it's doing and everything, but I just don't understand PL/pgSQL well enough to make it work.
My attempt is
CREATE FUNCTION near(VARCHAR, INTEGER) RETURNS TABLE(fromID INT,toID INT,jumps INT,name VARCHAR,security VARCHAR) AS $$
DECLARE --Declaration from here http://www.postgresql.org/docs/9.1/static/plpgsql-declarations.html
MiddleSystemName ALIAS FOR $1;
jumpsAway ALIAS FOR $2;
jumps INTEGER :=1;
BEGIN
--http://stackoverflow.com/questions/11979154/select-into-to-create-a-table-in-pl-pgsql
CREATE TEMP TABLE map AS
SELECT -1, mSS.solarSystemID, 0
FROM mapSolarSystems mSS
WHERE mSS.solarSystemName= MiddleSystemName;
LOOP
--http://www.postgresql.org/docs/9.1/static/plpgsql-statements.html#PLPGSQL-STATEMENTS-EXECUTING-DYN
--If you don't do it with execute, you can only do one row, I guess?
EXECUTE 'SELECT
|| mSSJ.fromSolarSystemID,
|| mSSJ.toSolarSystemID,
|| $1
|| FROM
|| mapSolarSystemJumps mSSJ
|| WHERE
|| mSSJ.fromSolarSystemID EXISTS (SELECT toSolarSystemID FROM map WHERE jumps = $1 - 1)
|| AND mSSJ.fromSolarSystemID NOT EXISTS (SELECT fromSolarSystemID FROM map)'
INTO map
USING jumps;
jumps := jumps + 1
EXIT WHEN jumps > jumpsAway;
END LOOP;
RETURN QUERY SELECT m.*,mSS.solarSystemName, mSS.security FROM JOIN mapSolarSystems mSS ON m.toSolarSystemID = mSS.solarSystemID;
END;
$$ LANGUAGE plpgsql;
And the error that produces is
Error is
ERROR: "map" is not a known variable
LINE 27: INTO map
^
Thanks for all the help.
PL/pgSQL
This should be a valid translation to plpgsql:
CREATE OR REPLACE FUNCTION f_near(_middlesystemname text, _jumpsaway int)
RETURNS TABLE(fromid int, toid int, jumps int, name text, security text) AS
$func$
DECLARE
_jumps integer;
BEGIN
CREATE TEMP TABLE map AS
SELECT -1 AS "fromSolarSystemID"
,m."solarSystemID" AS "toSolarSystemID"
,0 AS level
FROM "mapSolarSystems" m
WHERE "solarSystemName" = _middlesystemname;
-- potentially add indexes on the temp table and ANALYZE if it gets big
FOR _jumps IN 1 .. _jumpsaway LOOP
INSERT INTO map ("fromSolarSystemID", "toSolarSystemID", level)
SELECT sj."fromSolarSystemID", sj."toSolarSystemID", _jumps AS level
FROM "mapSolarSystemJumps" sj
JOIN map m ON m."toSolarSystemID" = sj."fromSolarSystemID"
AND m."level" = _jumps - 1
LEFT JOIN map mx ON mx."fromSolarSystemID" = sj."fromSolarSystemID"
WHERE mx."fromSolarSystemID" IS NULL;
END LOOP;
RETURN QUERY
SELECT m.*, s."solarSystemName", s."security"
FROM map m
JOIN "mapSolarSystems" s ON m."toSolarSystemID" = s."solarSystemID";
END
$func$ LANGUAGE plpgsql;
RECURSIVE CTE - doesn't seem to work
This short SQL query with a recursive CTE should have done it:
WITH RECURSIVE map AS (
SELECT -1 AS fromsolarsystemid, m.solarsystemid, 0 AS level
FROM mapsolarsystems m
WHERE m.solarsystemname = from_id
UNION ALL
SELECT sj.fromsolarsystemid, sj.tosolarsystemid, level + 1
FROM mapsolarsystemjumps sj
JOIN map m USING (level)
LEFT JOIN map mx USING (fromsolarsystemid)
WHERE sj.fromsolarsystemid = m.tosolarsystemid
AND mx.fromsolarsystemid IS NULL
AND m.level < 10 -- jumpsAway
)
SELECT m.*, s.solarsystemname, s.security
FROM map m
JOIN mapsolarsystems s ON m.tosolarsystemid = s.solarsystemid
-- WHERE s.security < 0.45 -- uncomment to check all nearby lowsec system
However:
ERROR: recursive reference to query "map" must not appear within an outer join
LINE 9: LEFT JOIN map mx USING (fromsolarsystemid)