SET citus.next_shard_id TO 20020000;
SET client_min_messages TO ERROR;
CREATE USER functionuser;
SELECT 1 FROM run_command_on_workers($$CREATE USER functionuser;$$);
 ?column?
---------------------------------------------------------------------
        1
        1
(2 rows)

RESET client_min_messages;
CREATE SCHEMA function_tests AUTHORIZATION functionuser;
CREATE SCHEMA function_tests2 AUTHORIZATION functionuser;
SET search_path TO function_tests;
SET citus.shard_count TO 4;
-- test notice
CREATE TABLE notices (
    id int primary key,
    message text
);
SELECT create_distributed_table('notices', 'id');
 create_distributed_table
---------------------------------------------------------------------

(1 row)

INSERT INTO notices VALUES (1, 'hello world');
CREATE FUNCTION notice(text)
RETURNS void
LANGUAGE plpgsql AS $$
BEGIN
    RAISE NOTICE '%', $1;
END;
$$;
SELECT create_distributed_function('notice(text)');
NOTICE:  procedure function_tests.notice is already distributed
DETAIL:  Citus distributes procedures with CREATE [PROCEDURE|FUNCTION|AGGREGATE] commands
 create_distributed_function
---------------------------------------------------------------------

(1 row)

SELECT notice(message) FROM notices WHERE id = 1;
NOTICE:  hello world
DETAIL:  from localhost:xxxxx
 notice
---------------------------------------------------------------------

(1 row)

-- should not see a NOTICE if worker_min_messages is WARNING
SET citus.worker_min_messages TO WARNING;
SELECT notice(message) FROM notices WHERE id = 1;
 notice
---------------------------------------------------------------------

(1 row)

RESET citus.worker_min_messages;
-- Create and distribute a simple function
CREATE FUNCTION eq(macaddr, macaddr) RETURNS bool
    AS 'select $1 = $2;'
    LANGUAGE SQL
    IMMUTABLE
    RETURNS NULL ON NULL INPUT;
CREATE FUNCTION eq8(macaddr8, macaddr8) RETURNS bool
    AS 'select $1 = $2;'
    LANGUAGE SQL
    IMMUTABLE
    RETURNS NULL ON NULL INPUT;
-- $function$ is what postgres escapes functions with when deparsing
-- make sure $function$ doesn't cause invalid syntax
CREATE FUNCTION add_text(text, text) RETURNS text
    AS 'select $function$test$function$ || $1::int || $2::int;'
    LANGUAGE SQL
    IMMUTABLE
    RETURNS NULL ON NULL INPUT;
CREATE FUNCTION add_polygons(polygon, polygon) RETURNS int
    AS 'select 1'
    LANGUAGE SQL
    IMMUTABLE
    RETURNS NULL ON NULL INPUT;
CREATE FUNCTION agg_dummy_func(state int, item int)
RETURNS int IMMUTABLE LANGUAGE plpgsql AS $$
begin
    return state + item;
end;
$$;
SET client_min_messages TO WARNING;
-- will skip trying to propagate the aggregate due to temp schema
CREATE AGGREGATE pg_temp.dummy_agg(int) (
    sfunc = agg_dummy_func,
    stype = int,
    sspace = 8,
    finalfunc = agg_dummy_func,
    finalfunc_extra,
    initcond = '5',
    msfunc = agg_dummy_func,
    mstype = int,
    msspace = 12,
    minvfunc = agg_dummy_func,
    mfinalfunc = agg_dummy_func,
    mfinalfunc_extra,
    minitcond = '1',
    sortop = ">"
);
WARNING:  "function pg_temp_xxx.dummy_agg(integer)" has dependency on unsupported object "schema pg_temp_xxx"
DETAIL:  "function pg_temp_xxx.dummy_agg(integer)" will be created only locally
RESET client_min_messages;
-- Test some combination of functions without ddl propagation
-- This will prevent the workers from having those types created. They are
-- created just-in-time on function distribution
SET citus.enable_ddl_propagation TO off;
CREATE TYPE dup_result AS (f1 macaddr, f2 text);
CREATE FUNCTION dup(macaddr) RETURNS dup_result
    AS $$ SELECT $1, CAST($1 AS text) || ' is text' $$
    LANGUAGE SQL;
CREATE FUNCTION increment(int2) RETURNS int
    AS $$ SELECT $1 + 1$$
    LANGUAGE SQL;
CREATE FUNCTION eq_with_param_names(val1 macaddr, val2 macaddr) RETURNS bool
    AS 'select $1 = $2;'
    LANGUAGE SQL
    IMMUTABLE
    RETURNS NULL ON NULL INPUT;
CREATE FUNCTION add_without_param_names(integer, integer) RETURNS integer
    AS 'select $1 + $2;'
    LANGUAGE SQL
    IMMUTABLE
    RETURNS NULL ON NULL INPUT;
CREATE FUNCTION "eq_mi'xed_param_names"(macaddr, "va'l1" macaddr) RETURNS bool
    AS 'select $1 = $2;'
    LANGUAGE SQL
    IMMUTABLE
    RETURNS NULL ON NULL INPUT;
-- Include aggregate function case
CREATE FUNCTION agg_sfunc(state int, item int)
RETURNS int IMMUTABLE LANGUAGE plpgsql AS $$
begin
    return state + item;
end;
$$;
CREATE FUNCTION agg_invfunc(state int, item int)
RETURNS int IMMUTABLE LANGUAGE plpgsql AS $$
begin
    return state - item;
end;
$$;
CREATE FUNCTION agg_finalfunc(state int, extra int)
RETURNS int IMMUTABLE LANGUAGE plpgsql AS $$
begin
        return state * 2;
end;
$$;
CREATE AGGREGATE sum2(int) (
    sfunc = agg_sfunc,
    stype = int,
    sspace = 8,
    finalfunc = agg_finalfunc,
    finalfunc_extra,
    initcond = '5',
    msfunc = agg_sfunc,
    mstype = int,
    msspace = 12,
    minvfunc = agg_invfunc,
    mfinalfunc = agg_finalfunc,
    mfinalfunc_extra,
    minitcond = '1',
    sortop = ">"
);
-- Test VARIADIC, example taken from postgres test suite
CREATE AGGREGATE my_rank(VARIADIC "any" ORDER BY VARIADIC "any") (
    stype = internal,
    sfunc = ordered_set_transition_multi,
    finalfunc = rank_final,
    finalfunc_extra,
    hypothetical
);
-- Test deparsing multiple parameters with names
CREATE FUNCTION agg_names_sfunc(state dup_result, x dup_result, yz dup_result)
RETURNS dup_result IMMUTABLE STRICT LANGUAGE sql AS $$
    select x.f1 | yz.f1, x.f2 || yz.f2;
$$;
CREATE FUNCTION agg_names_finalfunc(x dup_result)
RETURNS int IMMUTABLE STRICT LANGUAGE plpgsql AS $$
begin
    return x.f1;
end;
$$;
CREATE AGGREGATE agg_names(x dup_result, yz dup_result) (
    stype = dup_result,
    sfunc = agg_names_sfunc,
    finalfunc = agg_names_finalfunc,
    finalfunc_modify = shareable
);
-- make sure to propagate ddl propagation after we have setup our functions, this will
-- allow alter statements to be propagated and keep the functions in sync across machines
SET citus.enable_ddl_propagation TO on;
-- use an unusual type to force a new colocation group
CREATE TABLE statement_table(id int2);
SET citus.shard_replication_factor TO 2;
SELECT create_distributed_table('statement_table','id');
 create_distributed_table
---------------------------------------------------------------------

(1 row)

-- create a table uses streaming-based replication (can be synced)
CREATE TABLE streaming_table(id macaddr);
SET citus.shard_replication_factor TO 1;
SELECT create_distributed_table('streaming_table','id');
 create_distributed_table
---------------------------------------------------------------------

(1 row)

-- if not paremeters are supplied, we'd see that function doesn't have
-- distribution_argument_index and colocationid
SELECT create_distributed_function('"eq_mi''xed_param_names"(macaddr, macaddr)');
 create_distributed_function
---------------------------------------------------------------------

(1 row)

SELECT distribution_argument_index is NULL, colocationid is NULL from pg_catalog.pg_dist_object
WHERE objid = 'eq_mi''xed_param_names(macaddr, macaddr)'::regprocedure;
 ?column? | ?column?
---------------------------------------------------------------------
 t        | t
(1 row)

-- also show that we can use the function
SELECT * FROM run_command_on_workers($$SELECT function_tests."eq_mi'xed_param_names"('0123456789ab','ba9876543210');$$) ORDER BY 1,2;
 nodename  | nodeport | success | result
---------------------------------------------------------------------
 localhost |    57637 | t       | f
 localhost |    57638 | t       | f
(2 rows)

-- try to co-locate with a table that uses statement-based replication
SELECT create_distributed_function('increment(int2)', '$1');
ERROR:  cannot distribute the function "increment" since there is no table to colocate with
HINT:  Provide a distributed table via "colocate_with" option to create_distributed_function()
SELECT create_distributed_function('increment(int2)', '$1', colocate_with := 'statement_table');
ERROR:  cannot colocate function "increment" and table "statement_table"
DETAIL:  Citus currently only supports colocating function with distributed tables that are created using streaming replication model.
HINT:  When distributing tables make sure that citus.shard_replication_factor = 1
BEGIN;
DROP TABLE statement_table;
SELECT create_distributed_function('increment(int2)', '$1');
ERROR:  cannot distribute the function "increment" since there is no table to colocate with
HINT:  Provide a distributed table via "colocate_with" option to create_distributed_function()
END;
-- try to co-locate with a table that uses streaming replication
SELECT create_distributed_function('dup(macaddr)', '$1', colocate_with := 'streaming_table');
 create_distributed_function
---------------------------------------------------------------------

(1 row)

SELECT * FROM run_command_on_workers($$SELECT function_tests.dup('0123456789ab');$$) ORDER BY 1,2;
 nodename  | nodeport | success |                     result
---------------------------------------------------------------------
 localhost |    57637 | t       | (01:23:45:67:89:ab,"01:23:45:67:89:ab is text")
 localhost |    57638 | t       | (01:23:45:67:89:ab,"01:23:45:67:89:ab is text")
(2 rows)

SELECT create_distributed_function('eq(macaddr,macaddr)', '$1', colocate_with := 'streaming_table');
 create_distributed_function
---------------------------------------------------------------------

(1 row)

SELECT * FROM run_command_on_workers($$SELECT function_tests.eq('012345689ab','0123456789ab');$$) ORDER BY 1,2;
 nodename  | nodeport | success | result
---------------------------------------------------------------------
 localhost |    57637 | t       | f
 localhost |    57638 | t       | f
(2 rows)

SELECT public.verify_function_is_same_on_workers('function_tests.eq(macaddr,macaddr)');
 verify_function_is_same_on_workers
---------------------------------------------------------------------
 t
(1 row)

-- distribute aggregate
SELECT create_distributed_function('sum2(int)');
 create_distributed_function
---------------------------------------------------------------------

(1 row)

SELECT create_distributed_function('my_rank("any")');
 create_distributed_function
---------------------------------------------------------------------

(1 row)

SELECT create_distributed_function('agg_names(dup_result,dup_result)');
 create_distributed_function
---------------------------------------------------------------------

(1 row)

-- testing alter statements for a distributed function
-- ROWS 5, untested because;
-- ERROR:  ROWS is not applicable when function does not return a set
ALTER FUNCTION eq(macaddr,macaddr) CALLED ON NULL INPUT IMMUTABLE SECURITY INVOKER PARALLEL UNSAFE LEAKPROOF COST 5;
SELECT public.verify_function_is_same_on_workers('function_tests.eq(macaddr,macaddr)');
 verify_function_is_same_on_workers
---------------------------------------------------------------------
 t
(1 row)

ALTER FUNCTION eq(macaddr,macaddr) RETURNS NULL ON NULL INPUT STABLE SECURITY DEFINER PARALLEL RESTRICTED;
SELECT public.verify_function_is_same_on_workers('function_tests.eq(macaddr,macaddr)');
 verify_function_is_same_on_workers
---------------------------------------------------------------------
 t
(1 row)

ALTER FUNCTION eq(macaddr,macaddr) STRICT VOLATILE PARALLEL SAFE;
SELECT public.verify_function_is_same_on_workers('function_tests.eq(macaddr,macaddr)');
 verify_function_is_same_on_workers
---------------------------------------------------------------------
 t
(1 row)

-- Test SET/RESET for alter function
ALTER FUNCTION eq(macaddr,macaddr) SET client_min_messages TO warning;
SELECT public.verify_function_is_same_on_workers('function_tests.eq(macaddr,macaddr)');
 verify_function_is_same_on_workers
---------------------------------------------------------------------
 t
(1 row)

ALTER FUNCTION eq(macaddr,macaddr) SET client_min_messages TO error;
SELECT public.verify_function_is_same_on_workers('function_tests.eq(macaddr,macaddr)');
 verify_function_is_same_on_workers
---------------------------------------------------------------------
 t
(1 row)

ALTER ROUTINE eq(macaddr,macaddr) SET client_min_messages TO debug;
SELECT public.verify_function_is_same_on_workers('function_tests.eq(macaddr,macaddr)');
 verify_function_is_same_on_workers
---------------------------------------------------------------------
 t
(1 row)

ALTER FUNCTION eq(macaddr,macaddr) RESET client_min_messages;
SELECT public.verify_function_is_same_on_workers('function_tests.eq(macaddr,macaddr)');
 verify_function_is_same_on_workers
---------------------------------------------------------------------
 t
(1 row)

ALTER FUNCTION eq(macaddr,macaddr) SET search_path TO 'sch'';ma', public;
SELECT public.verify_function_is_same_on_workers('function_tests.eq(macaddr,macaddr)');
 verify_function_is_same_on_workers
---------------------------------------------------------------------
 t
(1 row)

ALTER FUNCTION eq(macaddr,macaddr) RESET search_path;
-- SET ... FROM CURRENT is not supported, verify the query fails with a descriptive error irregardless of where in the action list the statement occurs
ALTER FUNCTION eq(macaddr,macaddr) SET client_min_messages FROM CURRENT;
ERROR:  unsupported ALTER FUNCTION ... SET ... FROM CURRENT for a distributed function
HINT:  SET FROM CURRENT is not supported for distributed functions, instead use the SET ... TO ... syntax with a constant value.
SELECT public.verify_function_is_same_on_workers('function_tests.eq(macaddr,macaddr)');
 verify_function_is_same_on_workers
---------------------------------------------------------------------
 t
(1 row)

ALTER FUNCTION eq(macaddr,macaddr) RETURNS NULL ON NULL INPUT SET client_min_messages FROM CURRENT;
ERROR:  unsupported ALTER FUNCTION ... SET ... FROM CURRENT for a distributed function
HINT:  SET FROM CURRENT is not supported for distributed functions, instead use the SET ... TO ... syntax with a constant value.
SELECT public.verify_function_is_same_on_workers('function_tests.eq(macaddr,macaddr)');
 verify_function_is_same_on_workers
---------------------------------------------------------------------
 t
(1 row)

ALTER FUNCTION eq(macaddr,macaddr) SET client_min_messages FROM CURRENT SECURITY DEFINER;
ERROR:  unsupported ALTER FUNCTION ... SET ... FROM CURRENT for a distributed function
HINT:  SET FROM CURRENT is not supported for distributed functions, instead use the SET ... TO ... syntax with a constant value.
SELECT public.verify_function_is_same_on_workers('function_tests.eq(macaddr,macaddr)');
 verify_function_is_same_on_workers
---------------------------------------------------------------------
 t
(1 row)

-- rename function and make sure the new name can be used on the workers while the old name can't
ALTER FUNCTION eq(macaddr,macaddr) RENAME TO eq2;
SELECT public.verify_function_is_same_on_workers('function_tests.eq2(macaddr,macaddr)');
 verify_function_is_same_on_workers
---------------------------------------------------------------------
 t
(1 row)

SELECT * FROM run_command_on_workers($$SELECT function_tests.eq('012346789ab','012345689ab');$$) ORDER BY 1,2;
 nodename  | nodeport | success |                               result
---------------------------------------------------------------------
 localhost |    57637 | f       | ERROR:  function function_tests.eq(unknown, unknown) does not exist
 localhost |    57638 | f       | ERROR:  function function_tests.eq(unknown, unknown) does not exist
(2 rows)

SELECT * FROM run_command_on_workers($$SELECT function_tests.eq2('012345689ab','012345689ab');$$) ORDER BY 1,2;
 nodename  | nodeport | success | result
---------------------------------------------------------------------
 localhost |    57637 | t       | t
 localhost |    57638 | t       | t
(2 rows)

ALTER ROUTINE eq2(macaddr,macaddr) RENAME TO eq;
ALTER AGGREGATE sum2(int) RENAME TO sum27;
SELECT * FROM run_command_on_workers($$SELECT 1 from pg_proc where proname = 'sum27';$$) ORDER BY 1,2;
 nodename  | nodeport | success | result
---------------------------------------------------------------------
 localhost |    57637 | t       | 1
 localhost |    57638 | t       | 1
(2 rows)

ALTER AGGREGATE sum27(int) RENAME TO sum2;
-- change the owner of the function and verify the owner has been changed on the workers
ALTER FUNCTION eq(macaddr,macaddr) OWNER TO functionuser;
SELECT public.verify_function_is_same_on_workers('function_tests.eq(macaddr,macaddr)');
 verify_function_is_same_on_workers
---------------------------------------------------------------------
 t
(1 row)

ALTER AGGREGATE sum2(int) OWNER TO functionuser;
ALTER ROUTINE my_rank("any") OWNER TO functionuser;
ALTER AGGREGATE my_rank("any") OWNER TO functionuser;
SELECT run_command_on_workers($$
SELECT array_agg(row(usename, nspname, proname) order by proname)
FROM pg_proc
JOIN pg_user ON (usesysid = proowner)
JOIN pg_namespace ON (pg_namespace.oid = pronamespace and nspname = 'function_tests')
WHERE proname IN ('eq', 'sum2', 'my_rank');
$$);
                                                            run_command_on_workers
---------------------------------------------------------------------
 (localhost,57637,t,"{""(functionuser,function_tests,eq)"",""(functionuser,function_tests,my_rank)"",""(functionuser,function_tests,sum2)""}")
 (localhost,57638,t,"{""(functionuser,function_tests,eq)"",""(functionuser,function_tests,my_rank)"",""(functionuser,function_tests,sum2)""}")
(2 rows)

-- change the schema of the function and verify the old schema doesn't exist anymore while
-- the new schema has the function.
ALTER FUNCTION eq(macaddr,macaddr) SET SCHEMA function_tests2;
SELECT public.verify_function_is_same_on_workers('function_tests2.eq(macaddr,macaddr)');
 verify_function_is_same_on_workers
---------------------------------------------------------------------
 t
(1 row)

SELECT * FROM run_command_on_workers($$SELECT function_tests.eq('0123456789ab','ba9876543210');$$) ORDER BY 1,2;
 nodename  | nodeport | success |                               result
---------------------------------------------------------------------
 localhost |    57637 | f       | ERROR:  function function_tests.eq(unknown, unknown) does not exist
 localhost |    57638 | f       | ERROR:  function function_tests.eq(unknown, unknown) does not exist
(2 rows)

SELECT * FROM run_command_on_workers($$SELECT function_tests2.eq('012345689ab','ba9876543210');$$) ORDER BY 1,2;
 nodename  | nodeport | success | result
---------------------------------------------------------------------
 localhost |    57637 | t       | f
 localhost |    57638 | t       | f
(2 rows)

ALTER ROUTINE function_tests2.eq(macaddr,macaddr) SET SCHEMA function_tests;
ALTER AGGREGATE sum2(int) SET SCHEMA function_tests2;
-- when a function is distributed and we create or replace the function we need to propagate the statement to the worker to keep it in sync with the coordinator
CREATE OR REPLACE FUNCTION eq(macaddr, macaddr) RETURNS bool
AS 'select $1 <> $2;' -- I know, this is not an add, but the output will tell us if the update succeeded
    LANGUAGE SQL
    IMMUTABLE
    RETURNS NULL ON NULL INPUT;
SELECT public.verify_function_is_same_on_workers('function_tests.eq(macaddr,macaddr)');
 verify_function_is_same_on_workers
---------------------------------------------------------------------
 t
(1 row)

SELECT * FROM run_command_on_workers($$SELECT function_tests.eq('012345689ab','012345689ab');$$) ORDER BY 1,2;
 nodename  | nodeport | success | result
---------------------------------------------------------------------
 localhost |    57637 | t       | f
 localhost |    57638 | t       | f
(2 rows)

-- distributed functions should not be allowed to depend on an extension, also functions
-- that depend on an extension should not be allowed to be distributed.
ALTER FUNCTION eq(macaddr,macaddr) DEPENDS ON EXTENSION citus;
ERROR:  distrtibuted functions are not allowed to depend on an extension
DETAIL:  Function "function_tests.eq(pg_catalog.macaddr,pg_catalog.macaddr)" is already distributed. Functions from extensions are expected to be created on the workers by the extension they depend on.
SELECT create_distributed_function('pg_catalog.citus_drop_trigger()');
ERROR:  Citus extension functions(citus_drop_trigger) cannot be distributed.
DROP FUNCTION eq(macaddr,macaddr);
-- call should fail as function should have been dropped
SELECT * FROM run_command_on_workers($$SELECT function_tests.eq('0123456789ab','ba9876543210');$$) ORDER BY 1,2;
 nodename  | nodeport | success |                               result
---------------------------------------------------------------------
 localhost |    57637 | f       | ERROR:  function function_tests.eq(unknown, unknown) does not exist
 localhost |    57638 | f       | ERROR:  function function_tests.eq(unknown, unknown) does not exist
(2 rows)

-- Test DROP for ROUTINE
CREATE OR REPLACE FUNCTION eq(macaddr, macaddr) RETURNS bool
AS 'select $1 = $2;'
    LANGUAGE SQL
    IMMUTABLE
    RETURNS NULL ON NULL INPUT;
select create_distributed_function('eq(macaddr,macaddr)');
NOTICE:  procedure function_tests.eq is already distributed
DETAIL:  Citus distributes procedures with CREATE [PROCEDURE|FUNCTION|AGGREGATE] commands
 create_distributed_function
---------------------------------------------------------------------

(1 row)

DROP ROUTINE eq(macaddr, macaddr);
-- call should fail as function should have been dropped
SELECT * FROM run_command_on_workers($$SELECT function_tests.eq('0123456789ab','ba9876543210');$$) ORDER BY 1,2;
 nodename  | nodeport | success |                               result
---------------------------------------------------------------------
 localhost |    57637 | f       | ERROR:  function function_tests.eq(unknown, unknown) does not exist
 localhost |    57638 | f       | ERROR:  function function_tests.eq(unknown, unknown) does not exist
(2 rows)

DROP AGGREGATE function_tests2.sum2(int);
-- call should fail as aggregate should have been dropped
SELECT * FROM run_command_on_workers('SELECT function_tests2.sum2(id) FROM (select 1 id, 2) subq;') ORDER BY 1,2;
 nodename  | nodeport | success |                            result
---------------------------------------------------------------------
 localhost |    57637 | f       | ERROR:  function function_tests2.sum2(integer) does not exist
 localhost |    57638 | f       | ERROR:  function function_tests2.sum2(integer) does not exist
(2 rows)

-- invalid distribution_arg_name
SELECT create_distributed_function('eq_with_param_names(macaddr, macaddr)', distribution_arg_name:='test');
ERROR:  cannot distribute the function "eq_with_param_names" since the distribution argument is not valid
HINT:  Either provide a valid function argument name or a valid "$paramIndex" to create_distributed_function()
SELECT create_distributed_function('eq_with_param_names(macaddr, macaddr)', distribution_arg_name:='int');
ERROR:  cannot distribute the function "eq_with_param_names" since the distribution argument is not valid
HINT:  Either provide a valid function argument name or a valid "$paramIndex" to create_distributed_function()
-- invalid distribution_arg_index
SELECT create_distributed_function('eq_with_param_names(macaddr, macaddr)', '$0');
ERROR:  cannot distribute the function "eq_with_param_names" since the distribution argument is not valid
HINT:  Either provide a valid function argument name or a valid "$paramIndex" to create_distributed_function()
SELECT create_distributed_function('eq_with_param_names(macaddr, macaddr)', '$-1');
ERROR:  cannot distribute the function "eq_with_param_names" since the distribution argument is not valid
HINT:  Either provide a valid function argument name or a valid "$paramIndex" to create_distributed_function()
SELECT create_distributed_function('eq_with_param_names(macaddr, macaddr)', '$-10');
ERROR:  cannot distribute the function "eq_with_param_names" since the distribution argument is not valid
HINT:  Either provide a valid function argument name or a valid "$paramIndex" to create_distributed_function()
SELECT create_distributed_function('eq_with_param_names(macaddr, macaddr)', '$3');
ERROR:  cannot distribute the function "eq_with_param_names" since the distribution argument is not valid
HINT:  Either provide a valid function argument name or a valid "$paramIndex" to create_distributed_function()
SELECT create_distributed_function('eq_with_param_names(macaddr, macaddr)', '$1a');
ERROR:  invalid input syntax for type integer: "1a"
-- non existing column name
SELECT create_distributed_function('eq_with_param_names(macaddr, macaddr)', 'aaa');
ERROR:  cannot distribute the function "eq_with_param_names" since the distribution argument is not valid
HINT:  Either provide a valid function argument name or a valid "$paramIndex" to create_distributed_function()
-- NULL function
SELECT create_distributed_function(NULL);
ERROR:  the first parameter for create_distributed_function() should be a single a valid function or procedure name followed by a list of parameters in parantheses
HINT:  skip the parameters with OUT argtype as they are not part of the signature in PostgreSQL
-- NULL colocate_with
SELECT create_distributed_function('eq_with_param_names(macaddr, macaddr)', '$1', NULL);
ERROR:  colocate_with parameter should not be NULL
HINT:  To use the default value, set colocate_with option to "default"
-- empty string distribution_arg_index
SELECT create_distributed_function('eq_with_param_names(macaddr, macaddr)', '');
ERROR:  cannot distribute the function "eq_with_param_names" since the distribution argument is not valid
HINT:  Either provide a valid function argument name or a valid "$paramIndex" to create_distributed_function()
-- The first distributed function syncs the metadata to nodes
-- and metadata syncing is not supported within transaction blocks
BEGIN;
	SELECT create_distributed_function('eq_with_param_names(macaddr, macaddr)', distribution_arg_name:='val1');
 create_distributed_function
---------------------------------------------------------------------

(1 row)

ROLLBACK;
-- make sure that none of the nodes have the function because we've rollbacked
SELECT run_command_on_workers($$SELECT count(*) FROM pg_proc WHERE proname='eq_with_param_names';$$);
 run_command_on_workers
---------------------------------------------------------------------
 (localhost,57637,t,0)
 (localhost,57638,t,0)
(2 rows)

-- valid distribution with distribution_arg_name
SELECT create_distributed_function('eq_with_param_names(macaddr, macaddr)', distribution_arg_name:='val1');
 create_distributed_function
---------------------------------------------------------------------

(1 row)

-- make sure that both of the nodes have the function because we've succeeded
SELECT run_command_on_workers($$SELECT count(*) FROM pg_proc WHERE proname='eq_with_param_names';$$);
 run_command_on_workers
---------------------------------------------------------------------
 (localhost,57637,t,1)
 (localhost,57638,t,1)
(2 rows)

-- valid distribution with distribution_arg_name -- case insensitive
SELECT create_distributed_function('eq_with_param_names(macaddr, macaddr)', distribution_arg_name:='VaL1');
 create_distributed_function
---------------------------------------------------------------------

(1 row)

-- valid distribution with distribution_arg_index
SELECT create_distributed_function('eq_with_param_names(macaddr, macaddr)','$1');
 create_distributed_function
---------------------------------------------------------------------

(1 row)

-- a function cannot be colocated with a table that is not "streaming" replicated
SET citus.shard_replication_factor TO 2;
CREATE TABLE replicated_table_func_test (a macaddr);
SELECT create_distributed_table('replicated_table_func_test', 'a');
 create_distributed_table
---------------------------------------------------------------------

(1 row)

SELECT create_distributed_function('eq_with_param_names(macaddr, macaddr)', '$1', colocate_with:='replicated_table_func_test');
ERROR:  cannot colocate function "eq_with_param_names" and table "replicated_table_func_test"
DETAIL:  Citus currently only supports colocating function with distributed tables that are created using streaming replication model.
HINT:  When distributing tables make sure that citus.shard_replication_factor = 1
-- a function can be colocated with a different distribution argument type
-- as long as there is a coercion path
SET citus.shard_replication_factor TO 1;
CREATE TABLE replicated_table_func_test_2 (a macaddr8);
SELECT create_distributed_table('replicated_table_func_test_2', 'a');
 create_distributed_table
---------------------------------------------------------------------

(1 row)

SELECT create_distributed_function('eq_with_param_names(macaddr, macaddr)', 'val1', colocate_with:='replicated_table_func_test_2');
 create_distributed_function
---------------------------------------------------------------------

(1 row)

-- colocate_with cannot be used without distribution key
SELECT create_distributed_function('eq_with_param_names(macaddr, macaddr)', colocate_with:='replicated_table_func_test_2');
ERROR:  cannot distribute the function "eq_with_param_names" since the distribution argument is not valid
HINT:  To provide "colocate_with" option with a distributed table, the distribution argument parameter should also be provided
-- a function cannot be colocated with a local table
CREATE TABLE replicated_table_func_test_3 (a macaddr8);
SELECT create_distributed_function('eq_with_param_names(macaddr, macaddr)', 'val1', colocate_with:='replicated_table_func_test_3');
ERROR:  relation replicated_table_func_test_3 is not distributed
-- finally, colocate the function with a distributed table
SET citus.shard_replication_factor TO 1;
CREATE TABLE replicated_table_func_test_4 (a macaddr);
SELECT create_distributed_table('replicated_table_func_test_4', 'a');
 create_distributed_table
---------------------------------------------------------------------

(1 row)

SELECT create_distributed_function('eq_with_param_names(macaddr, macaddr)', '$1', colocate_with:='replicated_table_func_test_4');
 create_distributed_function
---------------------------------------------------------------------

(1 row)

-- show that the colocationIds are the same
SELECT pg_dist_partition.colocationid = objects.colocationid as table_and_function_colocated
FROM pg_dist_partition, pg_catalog.pg_dist_object as objects
WHERE pg_dist_partition.logicalrelid = 'replicated_table_func_test_4'::regclass AND
	  objects.objid = 'eq_with_param_names(macaddr, macaddr)'::regprocedure;
 table_and_function_colocated
---------------------------------------------------------------------
 t
(1 row)

-- now, redistributed with the default colocation option, we should still see that the same colocation
-- group preserved, because we're using the default shard creation settings
SELECT create_distributed_function('eq_with_param_names(macaddr, macaddr)', 'val1');
 create_distributed_function
---------------------------------------------------------------------

(1 row)

SELECT pg_dist_partition.colocationid = objects.colocationid as table_and_function_colocated
FROM pg_dist_partition, pg_catalog.pg_dist_object as objects
WHERE pg_dist_partition.logicalrelid = 'replicated_table_func_test_4'::regclass AND
	  objects.objid = 'eq_with_param_names(macaddr, macaddr)'::regprocedure;
 table_and_function_colocated
---------------------------------------------------------------------
 t
(1 row)

-- a function cannot be colocated with a single shard distributed table when a distribution column is provided
SELECT create_distributed_table('replicated_table_func_test_3', null);
 create_distributed_table
---------------------------------------------------------------------

(1 row)

SELECT create_distributed_function('eq_with_param_names(macaddr, macaddr)', 'val1', colocate_with:='replicated_table_func_test_3');
ERROR:  cannot colocate function "eq_with_param_names" and table "replicated_table_func_test_3" because distribution arguments are not supported when colocating with single shard distributed tables.
SELECT undistribute_table('replicated_table_func_test_3');
NOTICE:  creating a new table for function_tests.replicated_table_func_test_3
NOTICE:  moving the data of function_tests.replicated_table_func_test_3
NOTICE:  dropping the old function_tests.replicated_table_func_test_3
NOTICE:  renaming the new table to function_tests.replicated_table_func_test_3
 undistribute_table
---------------------------------------------------------------------

(1 row)

-- a function cannot be colocated with a reference table when a distribution column is provided
SELECT create_reference_table('replicated_table_func_test_3');
 create_reference_table
---------------------------------------------------------------------

(1 row)

SELECT create_distributed_function('eq_with_param_names(macaddr, macaddr)', 'val1', colocate_with:='replicated_table_func_test_3');
ERROR:  cannot colocate function "eq_with_param_names" and table "replicated_table_func_test_3" because distribution arguments are not supported when colocating with reference tables.
-- a function can be colocated with a reference table when the distribution argument is omitted
SELECT create_distributed_function('eq_with_param_names(macaddr, macaddr)', colocate_with:='replicated_table_func_test_3');
 create_distributed_function
---------------------------------------------------------------------

(1 row)

-- function with a macaddr8 dist. arg can be colocated with macaddr
-- column of a distributed table. In general, if there is a coercion
-- path, we rely on postgres for implicit coersions, and users for explicit coersions
-- to coerce the values
SELECT create_distributed_function('eq8(macaddr8, macaddr8)', '$1', colocate_with:='replicated_table_func_test_4');
 create_distributed_function
---------------------------------------------------------------------

(1 row)

SELECT pg_dist_partition.colocationid = objects.colocationid as table_and_function_colocated
FROM pg_dist_partition, pg_catalog.pg_dist_object as objects
WHERE pg_dist_partition.logicalrelid = 'replicated_table_func_test_4'::regclass AND
	  objects.objid = 'eq8(macaddr8, macaddr8)'::regprocedure;
 table_and_function_colocated
---------------------------------------------------------------------
 t
(1 row)

SELECT create_distributed_function('add_text(text, text)', '$1', colocate_with:='replicated_table_func_test_4');
 create_distributed_function
---------------------------------------------------------------------

(1 row)

SELECT pg_dist_partition.colocationid = objects.colocationid as table_and_function_colocated
FROM pg_dist_partition, pg_catalog.pg_dist_object as objects
WHERE pg_dist_partition.logicalrelid = 'replicated_table_func_test_4'::regclass AND
	  objects.objid = 'add_text(text, text)'::regprocedure;
 table_and_function_colocated
---------------------------------------------------------------------
 t
(1 row)

-- cannot distribute function because there is no
-- coercion path from polygon to int
SELECT create_distributed_function('add_polygons(polygon,polygon)', '$1', colocate_with:='replicated_table_func_test_4');
ERROR:  cannot colocate function "replicated_table_func_test_4" and table "add_polygons" because distribution column types don't match and there is no coercion path
-- without the colocate_with, the function errors out since there is no
-- default colocation group
SET citus.shard_count TO 55;
SELECT create_distributed_function('eq_with_param_names(macaddr, macaddr)', 'val1');
ERROR:  cannot distribute the function "eq_with_param_names" since there is no table to colocate with
HINT:  Provide a distributed table via "colocate_with" option to create_distributed_function()
SET citus.shard_replication_factor TO 1;
SET citus.shard_count TO 4;
CREATE TABLE test (id int, name text);
SELECT create_distributed_table('test','id');
 create_distributed_table
---------------------------------------------------------------------

(1 row)

INSERT INTO test VALUES (3,'three');
CREATE OR REPLACE FUNCTION increment(int)
RETURNS NUMERIC AS $$
DECLARE ret_val NUMERIC;
BEGIN
        SELECT max(id)::numeric+1 INTO ret_val  FROM test WHERE id = $1;
        RETURN ret_val;
END;
$$  LANGUAGE plpgsql;
SELECT create_distributed_function('increment(int)', '$1', colocate_with := 'test');
 create_distributed_function
---------------------------------------------------------------------

(1 row)

-- call a distributed function inside a pl/pgsql function
CREATE OR REPLACE FUNCTION test_func_calls_dist_func()
RETURNS NUMERIC AS $$
DECLARE incremented_val NUMERIC;
BEGIN
        SELECT INTO incremented_val increment(1);
        RETURN incremented_val;
END;
$$  LANGUAGE plpgsql;
SELECT test_func_calls_dist_func();
 test_func_calls_dist_func
---------------------------------------------------------------------

(1 row)

SELECT test_func_calls_dist_func();
 test_func_calls_dist_func
---------------------------------------------------------------------

(1 row)

-- test an INSERT..SELECT via the coordinator just because it is kind of funky
INSERT INTO test SELECT increment(3);
SELECT * FROM test ORDER BY id;
 id | name
---------------------------------------------------------------------
  3 | three
  4 |
(2 rows)

DROP TABLE test;
-- verify that recreating distributed functions with TABLE params gets propagated to workers
CREATE OR REPLACE FUNCTION func_with_return_table(int)
RETURNS  TABLE (date date)
LANGUAGE plpgsql AS $$
BEGIN
    RETURN query SELECT '2011-01-01'::date;
END;
$$;
SELECT create_distributed_function('func_with_return_table(int)');
NOTICE:  procedure function_tests.func_with_return_table is already distributed
DETAIL:  Citus distributes procedures with CREATE [PROCEDURE|FUNCTION|AGGREGATE] commands
 create_distributed_function
---------------------------------------------------------------------

(1 row)

CREATE OR REPLACE FUNCTION func_with_return_table(int)
RETURNS  TABLE (date date)
LANGUAGE plpgsql AS $$
BEGIN
    RETURN query SELECT '2011-01-02'::date;
END;
$$;
SELECT count(*) FROM
  (SELECT result FROM
    run_command_on_workers($$select row(pg_proc.pronargs, pg_proc.proargtypes, pg_proc.prosrc) from pg_proc where proname = 'func_with_return_table';$$)
    UNION  select row(pg_proc.pronargs, pg_proc.proargtypes, pg_proc.prosrc)::text from pg_proc where proname = 'func_with_return_table')
  as test;
 count
---------------------------------------------------------------------
     1
(1 row)

-- verify that recreating distributed functions with OUT params gets propagated to workers
CREATE OR REPLACE FUNCTION func_with_out_param(a int, out b int)
  RETURNS int
LANGUAGE sql AS $$ select 1; $$;
SELECT create_distributed_function('func_with_out_param(int)');
NOTICE:  procedure function_tests.func_with_out_param is already distributed
DETAIL:  Citus distributes procedures with CREATE [PROCEDURE|FUNCTION|AGGREGATE] commands
 create_distributed_function
---------------------------------------------------------------------

(1 row)

SET client_min_messages TO ERROR;
CREATE ROLE r1;
SELECT 1 FROM run_command_on_workers($$CREATE ROLE r1;$$);
 ?column?
---------------------------------------------------------------------
        1
        1
(2 rows)

GRANT EXECUTE ON FUNCTION func_with_out_param TO r1;
SELECT 1 FROM run_command_on_workers($$GRANT EXECUTE ON FUNCTION func_with_out_param TO r1;$$);
 ?column?
---------------------------------------------------------------------
        1
        1
(2 rows)

RESET client_min_messages;
CREATE OR REPLACE FUNCTION func_with_out_param(a int, out b int)
  RETURNS int
LANGUAGE sql AS $$ select 2; $$;
SELECT count(*) FROM
  (SELECT result FROM
    run_command_on_workers($$select row(pg_proc.pronargs, pg_proc.proargtypes, pg_proc.prosrc, pg_proc.proowner) from pg_proc where proname = 'func_with_out_param';$$)
    UNION  select row(pg_proc.pronargs, pg_proc.proargtypes, pg_proc.prosrc, pg_proc.proowner)::text from pg_proc where proname = 'func_with_out_param')
  as test;
 count
---------------------------------------------------------------------
     1
(1 row)

-- verify that recreating distributed functions with INOUT params gets propagated to workers
CREATE OR REPLACE FUNCTION func_with_inout_param(a int, inout b int)
  RETURNS int
LANGUAGE sql AS $$ select 1; $$;
-- this should error out
SELECT create_distributed_function('func_with_inout_param(int)');
ERROR:  function "func_with_inout_param(int)" does not exist
-- this should work
SELECT create_distributed_function('func_with_inout_param(int,int)');
NOTICE:  procedure function_tests.func_with_inout_param is already distributed
DETAIL:  Citus distributes procedures with CREATE [PROCEDURE|FUNCTION|AGGREGATE] commands
 create_distributed_function
---------------------------------------------------------------------

(1 row)

CREATE OR REPLACE FUNCTION func_with_inout_param(a int, inout b int)
  RETURNS int
LANGUAGE sql AS $$ select 2; $$;
SELECT count(*) FROM
  (SELECT result FROM
    run_command_on_workers($$select row(pg_proc.pronargs, pg_proc.proargtypes, pg_proc.prosrc) from pg_proc where proname = 'func_with_inout_param';$$)
    UNION  select row(pg_proc.pronargs, pg_proc.proargtypes, pg_proc.prosrc)::text from pg_proc where proname = 'func_with_inout_param')
  as test;
 count
---------------------------------------------------------------------
     1
(1 row)

-- verify that recreating distributed functions with VARIADIC params gets propagated to workers
CREATE OR REPLACE FUNCTION func_with_variadic_param(a int, variadic b int[])
  RETURNS int
LANGUAGE sql AS $$ select 1; $$;
-- this should work
SELECT create_distributed_function('func_with_variadic_param(int,int[])');
NOTICE:  procedure function_tests.func_with_variadic_param is already distributed
DETAIL:  Citus distributes procedures with CREATE [PROCEDURE|FUNCTION|AGGREGATE] commands
 create_distributed_function
---------------------------------------------------------------------

(1 row)

CREATE OR REPLACE FUNCTION func_with_variadic_param(a int, variadic b int[])
  RETURNS int
LANGUAGE sql AS $$ select 2; $$;
SELECT count(*) FROM
  (SELECT result FROM
    run_command_on_workers($$select row(pg_proc.pronargs, pg_proc.proargtypes, pg_proc.prosrc) from pg_proc where proname = 'func_with_variadic_param';$$)
    UNION  select row(pg_proc.pronargs, pg_proc.proargtypes, pg_proc.prosrc)::text from pg_proc where proname = 'func_with_variadic_param')
  as test;
 count
---------------------------------------------------------------------
     1
(1 row)

-- verify that recreating distributed functions returning setof records gets propagated to workers
CREATE OR REPLACE FUNCTION func_returning_setof_int(IN parm1 date, IN parm2 interval)
  RETURNS SETOF integer AS
$BODY$
BEGIN
    RETURN QUERY
    SELECT 1;
END;
$BODY$
  LANGUAGE plpgsql VOLATILE
  COST 100;
SELECT create_distributed_function('func_returning_setof_int(date,interval)');
NOTICE:  procedure function_tests.func_returning_setof_int is already distributed
DETAIL:  Citus distributes procedures with CREATE [PROCEDURE|FUNCTION|AGGREGATE] commands
 create_distributed_function
---------------------------------------------------------------------

(1 row)

CREATE OR REPLACE FUNCTION func_returning_setof_int(IN parm1 date, IN parm2 interval)
  RETURNS SETOF integer AS
$BODY$
BEGIN
    RETURN QUERY
    SELECT 2;

END;
$BODY$
  LANGUAGE plpgsql VOLATILE
  COST 100;
SELECT count(*) FROM
  (SELECT result FROM
    run_command_on_workers($$select row(pg_proc.pronargs, pg_proc.proargtypes, pg_proc.prosrc) from pg_proc where proname = 'func_returning_setof_int';$$)
    UNION  select row(pg_proc.pronargs, pg_proc.proargtypes, pg_proc.prosrc)::text from pg_proc where proname = 'func_returning_setof_int')
  as test;
 count
---------------------------------------------------------------------
     1
(1 row)

-- verify that recreating distributed functions with variadic param returning setof records gets propagated to workers
CREATE OR REPLACE FUNCTION func_returning_setof_int_with_variadic_param(IN parm1 date, VARIADIC parm2 int[])
  RETURNS SETOF integer AS
$BODY$
BEGIN
    RETURN QUERY
    SELECT 1;
END;
$BODY$
  LANGUAGE plpgsql VOLATILE
  COST 100;
SELECT create_distributed_function('func_returning_setof_int_with_variadic_param(date,int[])');
NOTICE:  procedure function_tests.func_returning_setof_int_with_variadic_param is already distributed
DETAIL:  Citus distributes procedures with CREATE [PROCEDURE|FUNCTION|AGGREGATE] commands
 create_distributed_function
---------------------------------------------------------------------

(1 row)

CREATE OR REPLACE FUNCTION func_returning_setof_int_with_variadic_param(IN parm1 date, VARIADIC parm2 int[])
  RETURNS SETOF integer AS
$BODY$
BEGIN
    RETURN QUERY
    SELECT 2;
END;
$BODY$
  LANGUAGE plpgsql VOLATILE
  COST 100;
SELECT count(*) FROM
  (SELECT result FROM
    run_command_on_workers($$select row(pg_proc.pronargs, pg_proc.proargtypes, pg_proc.prosrc) from pg_proc where proname = 'func_returning_setof_int_with_variadic_param';$$)
    UNION  select row(pg_proc.pronargs, pg_proc.proargtypes, pg_proc.prosrc)::text from pg_proc where proname = 'func_returning_setof_int_with_variadic_param')
  as test;
 count
---------------------------------------------------------------------
     1
(1 row)

-- verify that recreating distributed procedures with out params gets propagated to workers
CREATE OR REPLACE PROCEDURE proc_with_variadic_param(IN parm1 date, VARIADIC parm2 int[])
  LANGUAGE SQL
AS $$
    SELECT 1;
$$;
-- this should error out
SELECT create_distributed_function('proc_with_variadic_param(date)');
ERROR:  function "proc_with_variadic_param(date)" does not exist
-- this should work
SELECT create_distributed_function('proc_with_variadic_param(date,int[])');
NOTICE:  procedure function_tests.proc_with_variadic_param is already distributed
DETAIL:  Citus distributes procedures with CREATE [PROCEDURE|FUNCTION|AGGREGATE] commands
 create_distributed_function
---------------------------------------------------------------------

(1 row)

CREATE OR REPLACE PROCEDURE proc_with_variadic_param(IN parm1 date, VARIADIC parm2 int[])
  LANGUAGE SQL
AS $$
    SELECT 2;
$$;
SELECT count(*) FROM
  (SELECT result FROM
    run_command_on_workers($$select row(pg_proc.pronargs, pg_proc.proargtypes, pg_proc.prosrc) from pg_proc where proname = 'proc_with_variadic_param';$$)
    UNION  select row(pg_proc.pronargs, pg_proc.proargtypes, pg_proc.prosrc)::text from pg_proc where proname = 'proc_with_variadic_param')
  as test;
 count
---------------------------------------------------------------------
     1
(1 row)

-- verify that recreating distributed procedures with INOUT param gets propagated to workers
CREATE OR REPLACE PROCEDURE proc_with_inout_param(IN parm1 date, INOUT parm2 int)
  LANGUAGE SQL
AS $$
    SELECT 1;
$$;
-- this should error out
SELECT create_distributed_function('proc_with_inout_param(date)');
ERROR:  function "proc_with_inout_param(date)" does not exist
-- this should work
SELECT create_distributed_function('proc_with_inout_param(date,int)');
NOTICE:  procedure function_tests.proc_with_inout_param is already distributed
DETAIL:  Citus distributes procedures with CREATE [PROCEDURE|FUNCTION|AGGREGATE] commands
 create_distributed_function
---------------------------------------------------------------------

(1 row)

CREATE OR REPLACE PROCEDURE proc_with_inout_param(IN parm1 date, INOUT parm2 int)
  LANGUAGE SQL
AS $$
    SELECT 2;
$$;
SELECT count(*) FROM
  (SELECT result FROM
    run_command_on_workers($$select row(pg_proc.pronargs, pg_proc.proargtypes, pg_proc.prosrc) from pg_proc where proname = 'proc_with_inout_param';$$)
    UNION  select row(pg_proc.pronargs, pg_proc.proargtypes, pg_proc.prosrc)::text from pg_proc where proname = 'proc_with_inout_param')
  as test;
 count
---------------------------------------------------------------------
     1
(1 row)

SET client_min_messages TO error; -- suppress cascading objects dropping
DROP SCHEMA function_tests CASCADE;
DROP SCHEMA function_tests2 CASCADE;
-- clear objects
SELECT stop_metadata_sync_to_node(nodename,nodeport) FROM pg_dist_node WHERE isactive AND noderole = 'primary' AND groupid <> 0;
 stop_metadata_sync_to_node
---------------------------------------------------------------------


(2 rows)

-- This is hacky, but we should clean-up the resources as below
\c - - - :worker_1_port
UPDATE pg_dist_local_group SET groupid = 0;
TRUNCATE pg_dist_node;
\c - - - :worker_2_port
UPDATE pg_dist_local_group SET groupid = 0;
TRUNCATE pg_dist_node;
\c - - - :master_port
SET client_min_messages TO ERROR;
DROP USER functionuser;
DROP ROLE r1;
SELECT 1 FROM run_command_on_workers($$DROP USER functionuser$$);
 ?column?
---------------------------------------------------------------------
        1
        1
(2 rows)

-- sync metadata again
SELECT start_metadata_sync_to_node(nodename,nodeport) FROM pg_dist_node WHERE isactive AND noderole = 'primary' AND groupid <> 0;
 start_metadata_sync_to_node
---------------------------------------------------------------------


(2 rows)

