I'm trying to call a table valued function with some variable inputs. However, I'd like to use the function's default value if the calling script doesn't set the input value.
So, assume I have the function (yes, this is a silly function)
create function dbo.testing(#countTo int=10)
returns #output table(num int)
as
begin
declare #i int
set #i=0
while #i < #countTo
begin
set #i=#i+1
insert into #output(num)
values(#i)
end
return
end
Then, I could setup a variable for the input:
declare #stop int
set #stop=15
select * from dbo.testing(#stop)
However, if #stop is left as null, I'd like to use the default value, but it will just use null (which returns nothing...). Essentially, I want the following to work:
select * from dbo.testing(isnull(#stop,default))
But it just returns the error:
Msg 156, Level 15, State 1, Line 4 Incorrect syntax near the keyword
'default'.
Is there anyway to do this without modifying the function?
After researching this a bit my conclusion (I could be wrong though) is that you can't do this without either modifying the function as described below or include a conditional null check when you call the function as:
if #stop is null
select * from dbo.testing(default)
else
select * from dbo.testing(#stop)
This might not be practical though. The alternative is to modify your function to include a null check and default value:
create function dbo.testing(#countTo int)
returns #output table(num int)
as
begin
if #countTo is null set #countto = 10
declare #i int
set #i=0
while #i < #countTo
begin
set #i=#i+1
insert into #output(num)
values(#i)
end
return
end
And call it like select * from dbo.testing(isnull(#stop,null))
Related
I have defined a domain derived from a composite type that has a CHECK constraint to prevent one of its component values from ever being null. I want to write a plpgsql-language function that has a variable of this type. I am failing to declare such a variable.
Here is a simplified example of a scenario that gives me the same error message:
CREATE DOMAIN myint AS integer
CONSTRAINT not_null CHECK (VALUE IS NOT NULL);
CREATE FUNCTION myfunc() RETURNS myint LANGUAGE plpgsql AS $$
DECLARE
notnullint myint;
BEGIN
notnullint := 1;
RETURN notnullint;
END
$$;
mydb=# select myfunc();
ERROR: value for domain myint violates check constraint "not_null"
CONTEXT: PL/pgSQL function myfunc() line 4 during statement
block local variable initialization
Am I confused to be thinking it's impossible to bind a plpgsql variable to a value of a domain type that has a non null constraint? It's hard for me to believe PostgreSQL would have such a limitation.
Note: in my actual application I want to assign the variable the return value of an INSERT statement, so assigning the value within the DECLARE block seems inappropriate.
You can initialize variables in the DECLARE section:
CREATE FUNCTION myfunc() RETURNS myint LANGUAGE plpgsql AS $$
DECLARE
notnullint myint = 0;
BEGIN
notnullint := 1;
RETURN notnullint;
END
$$;
Note: in my actual application I want to assign the variable the return value of an INSERT statement, so assigning the value within the DECLARE block seems inappropriate.
I do not understand this logic. In the case, the variable has to be initialized and you rather have no choice. Of course, you can assign another value to it in the function body.
I am trying to create a Redshift UDF with function Parameters as below:
create or replace function function1(srctimezone VARCHAR,desttimezone VARCHAR,flag = 'nr') returns datetime
The last Parameter would be a defualt Parameter, i.e if user does not pass any value, it should take 'nr' by default. But I am not able to create function with default parameters. Any suggestions if Redshfit does / does not allow function creation with default Parameters. If it does, then what will be the correct syntax for the same?
In Redshift, you can create a Python UDF with an arbitrary number of arguments but you have to pass the same number of arguments when you execute the function. The workaround for optional parameters would be passing nulls and setting the parameter to the default value at the beginning of the function body similar to this:
create function f_optional_test(a int, b int)
returns int
stable as $$
if b==None:
return 999 # set default
else:
return b
$$ language plpythonu;
select f_optional_test(1,2); -- returns 2
select f_optional_test(1,null); -- returns 999
Also, you can create multiple functions with the same name but different number of parameters, so the function that is selected for execution will be picked up by the database engine based on the number of parameters and their data type:
create function f_optional_test(a int)
returns int
stable as $$
return 999 # default for b
$$ language plpythonu;
select f_optional_test(1,2); -- returns 2
select f_optional_test(1); -- returns 999
It's up to you to choose whether you'd like to have a single function at the expense of passing nulls for default parameters or have optional parameters at the expense of maintaining two versions of the same function if there is one optional parameter (with more optional parameters and their variable order it's more complicated and the first option is obviously better).
You can do:
CREATE OR REPLACE FUNCTION public.emulate_optional_arg(env_name varchar, optional varchar)
RETURNS varchar
LANGUAGE sql
IMMUTABLE
AS $$
SELECT $1 || ', ' || $2
$$
;
CREATE OR REPLACE FUNCTION public.emulate_optional_arg(env_name varchar)
RETURNS varchar
LANGUAGE sql
IMMUTABLE
AS $$
SELECT public.emulate_optional_arg($1,'default_value_here')
$$
;
SELECT public.emulate_optional_arg('dev');
/*
emulate_optional_arg |
-----------------------|
dev, default_value_here|
*/
SELECT public.emulate_optional_arg('dev','newvalue');
/*
emulate_optional_arg|
--------------------|
dev, newvalue |
*/
I have a very simple function to calculate and format a floating point number:
ALTER FUNCTION GetUrbanHours
(
-- Add the parameters for the function here
#driverID int
)
RETURNS int
AS
BEGIN
DECLARE #Result float = 0
SELECT #Result =
FORMAT(
SUM(CONVERT(float,
CONVERT(bigint, RouteSummary.UrbanNightSeconds + RouteSummary.UrbanDaySeconds))/(60*60)),
'##,##0.00')
FROM RouteSummary WHERE DriverID = #driverID
-- Return the result of the function
RETURN #Result
END
When I call the function with a given parameters, I get the following errors:
Msg 8115, Level 16, State 2, Line 6
Arithmetic overflow error converting expression to data type int.
Msg 8114, Level 16, State 5, Line 9
Error converting data type varchar to float.
When I extract the SELECT statement and print #Result, I get the expected result. What is my problem here?
EDIT
I have rewritten the function as follows:
ALTER FUNCTION GetUrbanHours
(
#driverID int
)
RETURNS nvarchar(50)
AS
BEGIN
DECLARE #Result nvarchar(50)
DECLARE #SumSeconds bigint
DECLARE #SumHours float
SELECT #SumSeconds =
SUM(RouteSummary.UrbanNightSeconds + RouteSummary.UrbanDaySeconds)
FROM RouteSummary WHERE DriverID = #driverID
IF #SumSeconds != NULL
BEGIN
SET #SumHours = CONVERT(float, #SumSeconds) / (60*60)
SET #Result =
FORMAT(#SumHours, '##,##0.00')
END
RETURN #Result
END
When I give a parameter that returns rows from the RouteSummary Table, I get NULL. When I give a parameter that returns no rows, I get "Msg 8115, Level 16, state 2, Line 1 Arithmetic overflow error converting expression to data type int." What is wrong now? Also is there a way to identify the line in the function the error message refers to? And why can't I step into the function with the SQL Studio debugger?
To solve the first error do not assign a value to the variable. The second error is caused by your use of the TSQL FORMAT statement. The format statement returns a VARCHAR not a FLOAT. See this link for information on the FORMAT command.
MSDN link
I am trying to write a PL/pgSQL function with optional arguments. It performs a query based on a filtered set of records (if specified), otherwise performs a query on the entire data set in a table.
For example (PSEUDO CODE):
CREATE OR REPLACE FUNCTION foofunc(param1 integer, param2 date, param2 date, optional_list_of_ids=[]) RETURNS SETOF RECORD AS $$
IF len(optional_list_of_ids) > 0 THEN
RETURN QUERY (SELECT * from foobar where f1=param1 AND f2=param2 AND id in optional_list_of_ids);
ELSE
RETURN QUERY (SELECT * from foobar where f1=param1 AND f2=param2);
ENDIF
$$ LANGUAGE SQL;
What would be the correct way to implement this function?
As an aside, I would like to know how I could call such a function in another outer function. This is how I would do it - is it correct, or is there a better way?
CREATE FUNCTION foofuncwrapper(param1 integer, param2 date, param2 date) RETURNS SETOF RECORD AS $$
BEGIN
CREATE TABLE ids AS SELECT id from foobar where id < 100;
RETURN QUERY (SELECT * FROM foofunc(param1, param2, ids));
END
$$ LANGUAGE SQL
Since PostgreSQL 8.4 (which you seem to be running), there are default values for function parameters. If you put your parameter last and provide a default, you can simply omit it from the call:
CREATE OR REPLACE FUNCTION foofunc(_param1 integer
, _param2 date
, _ids int[] DEFAULT '{}')
RETURNS SETOF foobar -- declare return type!
LANGUAGE plpgsql AS
$func$
BEGIN -- required for plpgsql
IF _ids <> '{}'::int[] THEN -- exclude empty array and NULL
RETURN QUERY
SELECT *
FROM foobar
WHERE f1 = _param1
AND f2 = _param2
AND id = ANY(_ids); -- "IN" is not proper syntax for arrays
ELSE
RETURN QUERY
SELECT *
FROM foobar
WHERE f1 = _param1
AND f2 = _param2;
END IF;
END -- required for plpgsql
$func$;
Major points:
The keyword DEFAULT is used to declare parameter defaults. Short alternative: =.
I removed the redundant param1 from the messy example.
Since you return SELECT * FROM foobar, declare the return type as RETURNS SETOF foobar instead of RETURNS SETOF record. The latter form with anonymous records is very unwieldy, you'd have to provide a column definition list with every call.
I use an array of integer (int[]) as function parameter. Adapted the IF expression and the WHERE clause accordingly.
IF statements are not available in plain SQL. Has to be LANGUAGE plpgsql for that.
Call with or without _ids:
SELECT * FROM foofunc(1, '2012-1-1'::date);
Effectively the same:
SELECT * FROM foofunc(1, '2012-1-1'::date, '{}'::int[]);
You have to make sure the call is unambiguous. If you have another function of the same name and two parameters, Postgres might not know which to pick. Explicit casting (like I demonstrate) narrows it down. Else, untyped string literals work, too, but being explicit never hurts.
Call from within another function:
CREATE FUNCTION foofuncwrapper(_param1 integer, _param2 date)
RETURNS SETOF foobar
LANGUAGE plgpsql AS
$func$
DECLARE
_ids int[] := '{1,2,3}';
BEGIN
-- whatever
RETURN QUERY
SELECT * FROM foofunc(_param1, _param2, _ids);
END
$func$;
Elaborating on Frank's answer on this thread:
The VARIADIC agument doesn't have to be the only argument, only the last one.
You can use VARIADIC for functions that may take zero variadic arguments, it's just a little fiddlier in that it requires a different calling style for zero args. You can provide a wrapper function to hide the ugliness. Given an initial varardic function definition like:
CREATE OR REPLACE FUNCTION foofunc(param1 integer, param2 date, param2 date, optional_list_of_ids VARIADIC integer[]) RETURNS SETOF RECORD AS $$
....
$$ language sql;
For zero args use a wrapper like:
CREATE OR REPLACE FUNCTION foofunc(integer, date, date) RETURNS SETOF RECORD AS $body$
SELECT foofunc($1,$2,$3,VARIADIC ARRAY[]::integer[]);
$body$ LANGUAGE 'sql';
or just call the main func with an empty array like VARIADIC '{}'::integer[] directly. The wrapper is ugly, but it's contained ugliness, so I'd recommend using a wrapper.
Direct calls can be made in variadic form:
SELECT foofunc(1,'2011-01-01','2011-01-01', 1, 2, 3, 4);
... or array call form with array ctor:
SELECT foofunc(1,'2011-01-01','2011-01-01', VARIADIC ARRAY[1,2,3,4]);
... or array text literal form:
SELECT foofunc(1,'2011-01-01','2011-01-01', VARIADIC '{1,2,3,4}'::int[]);
The latter two forms work with empty arrays.
You mean SQL Functions with Variable Numbers of Arguments? If so, use VARIADIC.
Pointedly what I'm asking below is: What is the actual data type of the #cleartext parameter of this SQL function? >> ENCRYPTBYKEY (..) -
http://msdn.microsoft.com/en-us/library/ms174361.aspx
(If you read below this line you can follow the history and reasoning. I think it's trickier than it first appears.)
The SQL Server documentation states the #cleartext (2nd) parameter of the EncryptByKey(..) function can accept a number of various types:
EncryptByKey (#key_GUID , #cleartext [, #add_authenticator, #authenticator] )
#cleartext Is a variable of type
nvarchar, char, varchar, binary,
varbinary, or nchar that contains data
that is to be encrypted with the key.
^ ^ ^ ^ ^ ^ ^ ^ ^ ^ - - - - - But what is its actual declared data type? ...
If I were to create a custom function (totally separate from the EncryptByKey example given above) what actual data type do I give a custom parameter so it will accept all those same types in the same manner?
Edit 1: I'm actually wrapping the SQL EncryptByKey function in a custom
UDF function and I want to recreate the same
parameter types to pass through to
it. This is the reasoning behind my want to create exact same params by type.
Edit 2: If I try using sql_variant it results in the error
Msg 8116, Level 16, State 1, Procedure
EncryptWrapper, Line 17 Argument data
type sql_variant is invalid for
argument 2 of EncryptByKey function.
Edit 3:
Here's my custom wrapper function - and the direct problem. What should the data type of #cleartext be for direct pass through to EncryptByKey?
ALTER FUNCTION [dbo].[EncryptWrapper]
(
#key_GUID uniqueidentifier,
#cleartext -- ??????????? <<< WHAT TYPE ????????????????
#add_authenticator int = 0,
#authenticator sysname = NULL
)
RETURNS varbinary(8000)
AS
BEGIN
-- //Calling a SQL Server builtin function.
-- //Second param #cleartext is the problem. What data type should it be?
Return EncryptByKey(#key_GUID, #cleartext, #add_authenticator, #authenticator)
END
Note: I shouldn't have to use CAST or CONVERT - I only need to use the proper data type for my #cleartext param.
Edit 4: Discovered the EncryptByKey(..) #cleartext parameter is not the following types:
sql_variant- raises error when passed
varbinary- too restrictive- doesn't allow passing of the text types otherwise accepted by EncryptByKey(..)
sysname, nvarchar, varchar- weird behaviour- tends to take only the first character of the argument text or something
try sql_variant:
CREATE FUNCTION [dbo].[yourFunction]
(
#InputStr sql_variant --can not be varchar(max) or nvarchar(max)
)
returns
varchar(8000)
BEGIN
--can use SQL_VARIANT_PROPERTY(#InputStr,'BaseType') to determine given datatype
--do whatever you want with #inputStr here
RETURN CONVERT(varchar(8000),#InputStr) --key is to convert the sql_varient to something you can use
END
GO
the key is to convert the sql_varient to something you can use within the function. you can use IF statements and check the BaseType and convert the sql_varient back into the native data type
EDIT
here is an example of how to get the original datatype:
CREATE FUNCTION [dbo].[yourFunction]
(
#InputStr sql_variant --can not be varchar(max) or nvarchar(max)
)
returns
varchar(8000)
BEGIN
DECLARE #Value varchar(50)
--can use SQL_VARIANT_PROPERTY(#InputStr,'BaseType') to determine given datatype
--do whatever you want with #inputStr here
IF #InputStr IS NULL
BEGIN
SET #value= 'was null'
END
ELSE IF SQL_VARIANT_PROPERTY(#InputStr,'BaseType')='char'
BEGIN
--your special code here
SET #value= 'char('+CONVERT(varchar(10),SQL_VARIANT_PROPERTY(#InputStr,'MaxLength '))+') - '+CONVERT(varchar(8000),#InputStr)
END
ELSE IF SQL_VARIANT_PROPERTY(#InputStr,'BaseType')='datetime'
BEGIN
--your special code here
SET #value= 'datetime - '+CONVERT(char(23),#InputStr,121)
END
ELSE IF SQL_VARIANT_PROPERTY(#InputStr,'BaseType')='nvarchar'
BEGIN
--your special code here
SET #value= 'nvarchar('+CONVERT(varchar(10),CONVERT(int,SQL_VARIANT_PROPERTY(#InputStr,'MaxLength '))/2)+') - '+CONVERT(varchar(8000),#InputStr)
END
ELSE
BEGIN
--your special code here
set #value= 'unknown!'
END
RETURN #value
END
GO
test it out:
DECLARE #x char(5), #z int, #d datetime, #n nvarchar(27)
SELECT #x='abc',#d=GETDATE(),#n='wow!'
select [dbo].[yourFunction](#x)
select [dbo].[yourFunction](#d)
select [dbo].[yourFunction](#z)
select [dbo].[yourFunction](#n)
test output:
-------------------------------------
char(5) - abc
(1 row(s) affected)
-------------------------------------
datetime - 2010-02-17 15:10:44.017
(1 row(s) affected)
-------------------------------------
was null
(1 row(s) affected)
-------------------------------------
nvarchar(27) - wow!
(1 row(s) affected)
ENCRYPTBYKEY() almost certainly isn't written in vanilla T-SQL. It doesn't need to follow T-SQL data typing rules.
That said, if you want to write a wrapper for it, use SQL_VARIANT for the #cleartext parameter, just as KM suggested.
If ENCRYPTBYKEY() is not sensitive to the max length of #cleartext, you could munge all CHAR/VARCHARs to VARCHAR(8000), and all NCHAR/NVARCHARs to NVACHAR(4000).
Otherwise you may be SOL: any data type conversion that respects maximum length--eg, CHAR(10) vs CHAR(20)--will require dynamic SQL, so you would have to write it as a stored procedure, rather than a function. At that point, it's not really a wrapper anymore.