# -*- coding: utf-8 -*- """Test functions""" import pytest from inspect import cleandoc from pyrseas.testutils import DatabaseToMapTestCase from pyrseas.testutils import InputMapToSqlTestCase, fix_indent SOURCE1 = "SELECT 'dummy'::text" CREATE_STMT1 = "CREATE FUNCTION sd.f1() RETURNS text LANGUAGE sql IMMUTABLE " \ "AS $_$%s$_$" % SOURCE1 SOURCE2 = "SELECT GREATEST($1, $2)" CREATE_STMT2 = "CREATE FUNCTION sd.f1(integer, integer) RETURNS integer " \ "LANGUAGE sql IMMUTABLE AS $_$%s$_$" % SOURCE2 COMMENT_STMT = "COMMENT ON FUNCTION sd.f1(integer, integer) IS " \ "'Test function f1'" SOURCE3 = "SELECT * FROM generate_series($1, $2)" CREATE_STMT3 = "CREATE FUNCTION f2(integer, integer) RETURNS SETOF integer " \ "ROWS 20 LANGUAGE sql IMMUTABLE AS $_$%s$_$" % SOURCE3 SOURCE4 = "SELECT $1 + $2" CREATE_STMT4 = "CREATE FUNCTION fadd(integer, integer) RETURNS integer " \ "LANGUAGE sql IMMUTABLE AS $_$%s$_$" % SOURCE4 SOURCE5 = "SELECT $1 - $2" CREATE_STMT5 = "CREATE FUNCTION fsub(integer, integer) RETURNS integer " \ "LANGUAGE sql IMMUTABLE AS $_$%s$_$" % SOURCE5 class FunctionToMapTestCase(DatabaseToMapTestCase): """Test mapping of existing functions""" def test_map_function1(self): "Map a very simple function with no arguments" dbmap = self.to_map([CREATE_STMT1]) expmap = {'language': 'sql', 'returns': 'text', 'source': SOURCE1, 'volatility': 'immutable'} assert dbmap['schema sd']['function f1()'] == expmap def test_map_function_with_args(self): "Map a function with two arguments" stmts = ["CREATE FUNCTION f1(integer, integer) RETURNS integer " "LANGUAGE sql AS $_$%s$_$" % SOURCE2] dbmap = self.to_map(stmts) assert dbmap['schema sd']['function f1(integer, integer)'] == \ {'language': 'sql', 'returns': 'integer', 'source': SOURCE2} def test_map_function_default_args(self): "Map a function with default arguments" stmts = ["CREATE FUNCTION f1(integer, integer) RETURNS integer " "LANGUAGE sql AS $_$%s$_$" % SOURCE2] dbmap = self.to_map(stmts) assert dbmap['schema sd']['function f1(integer, integer)'] == \ {'language': 'sql', 'returns': 'integer', 'source': SOURCE2} def test_map_void_function(self): "Map a function returning void" stmts = ["CREATE TABLE t1 (c1 integer, c2 text)", "CREATE FUNCTION f1() RETURNS void LANGUAGE sql AS " "$_$INSERT INTO t1 VALUES (1, 'dummy')$_$"] dbmap = self.to_map(stmts) expmap = {'language': 'sql', 'returns': 'void', 'source': "INSERT INTO t1 VALUES (1, 'dummy')"} assert dbmap['schema sd']['function f1()'] == expmap def test_map_setof_row_function(self): "Map a function returning a set of rows" stmts = ["CREATE TABLE t1 (c1 integer, c2 text)", "CREATE FUNCTION f1() RETURNS SETOF t1 LANGUAGE sql AS " "$_$SELECT * FROM t1$_$"] dbmap = self.to_map(stmts) expmap = {'language': 'sql', 'returns': 'SETOF sd.t1', 'source': "SELECT * FROM t1"} assert dbmap['schema sd']['function f1()'] == expmap def test_map_security_definer_function(self): "Map a function that is SECURITY DEFINER" stmts = ["CREATE FUNCTION f1() RETURNS text LANGUAGE sql " "SECURITY DEFINER AS $_$%s$_$" % SOURCE1] dbmap = self.to_map(stmts) expmap = {'language': 'sql', 'returns': 'text', 'source': SOURCE1, 'security_definer': True} assert dbmap['schema sd']['function f1()'] == expmap def test_map_c_lang_function(self): "Map a dynamically loaded C language function" # NOTE 1: Needs contrib/spi module to be available # NOTE 2: Needs superuser privilege stmts = ["CREATE FUNCTION autoinc() RETURNS trigger " "AS '$libdir/autoinc' LANGUAGE c"] dbmap = self.to_map(stmts, superuser=True) expmap = {'language': 'c', 'obj_file': '$libdir/autoinc', 'link_symbol': 'autoinc', 'returns': 'trigger'} assert dbmap['schema sd']['function autoinc()'] == expmap def test_map_function_config(self): "Map a function with a configuration parameter" stmts = ["CREATE FUNCTION f1() RETURNS date LANGUAGE sql SET " "datestyle to postgres, dmy AS $_$SELECT CURRENT_DATE$_$"] dbmap = self.to_map(stmts) expmap = {'language': 'sql', 'returns': 'date', 'configuration': ['DateStyle=postgres, dmy'], 'source': "SELECT CURRENT_DATE"} assert dbmap['schema sd']['function f1()'] == expmap def test_map_function_comment(self): "Map a function comment" dbmap = self.to_map([CREATE_STMT2, COMMENT_STMT]) assert dbmap['schema sd']['function f1(integer, integer)'][ 'description'] == 'Test function f1' def test_map_function_rows(self): "Map a function rows" dbmap = self.to_map([CREATE_STMT3]) assert dbmap['schema sd']['function f2(integer, integer)'][ 'rows'] == 20 def test_map_function_leakproof(self): "Map a function with LEAKPROOF qualifier" stmt = CREATE_STMT4.replace("IMMUTABLE", "IMMUTABLE LEAKPROOF") dbmap = self.to_map([stmt], superuser=True) expmap = {'language': 'sql', 'returns': 'integer', 'leakproof': True, 'source': SOURCE4, 'volatility': 'immutable'} assert dbmap['schema sd']['function fadd(integer, integer)'] == \ expmap class FunctionToSqlTestCase(InputMapToSqlTestCase): """Test SQL generation from input functions""" def test_create_function1(self): "Create a very simple function with no arguments" inmap = self.std_map() inmap['schema sd'].update({'function f1()': { 'language': 'sql', 'returns': 'text', 'source': SOURCE1, 'volatility': 'immutable'}}) sql = self.to_sql(inmap) assert fix_indent(sql[1]) == CREATE_STMT1 def test_create_function_with_args(self): "Create a function with two arguments" inmap = self.std_map() inmap['schema sd'].update({ 'function f1(integer, integer)': { 'language': 'sql', 'returns': 'integer', 'source': SOURCE2}}) sql = self.to_sql(inmap) assert fix_indent(sql[1]) == "CREATE FUNCTION sd.f1(integer, integer)"\ " RETURNS integer LANGUAGE sql AS $_$%s$_$" % SOURCE2 def test_create_setof_row_function(self): "Create a function returning a set of rows" inmap = self.std_map() inmap['schema sd'].update({'table t1': { 'columns': [{'c1': {'type': 'integer'}}, {'c2': {'type': 'text'}}]}}) inmap['schema sd'].update({ 'function f1()': {'language': 'sql', 'returns': 'SETOF t1', 'source': "SELECT * FROM t1"}}) sql = self.to_sql(inmap) assert fix_indent(sql[2]) == "CREATE FUNCTION sd.f1() RETURNS " \ "SETOF t1 LANGUAGE sql AS $_$SELECT * FROM t1$_$" def test_create_setof_row_function_rows(self): "Create a function returning a set of rows with suggested number" inmap = self.std_map() inmap['schema sd'].update({'table t1': { 'columns': [{'c1': {'type': 'integer'}}, {'c2': {'type': 'text'}}]}}) inmap['schema sd'].update({ 'function f1()': {'language': 'sql', 'returns': 'SETOF t1', 'source': "SELECT * FROM t1", 'rows': 50}}) sql = self.to_sql(inmap) assert fix_indent(sql[2]) == "CREATE FUNCTION sd.f1() RETURNS SETOF " \ "t1 LANGUAGE sql ROWS 50 AS $_$SELECT * FROM t1$_$" def test_create_security_definer_function(self): "Create a SECURITY DEFINER function" inmap = self.std_map() inmap['schema sd'].update({'function f1()': { 'language': 'sql', 'returns': 'text', 'source': SOURCE1, 'security_definer': True}}) sql = self.to_sql(inmap) assert fix_indent(sql[1]) == "CREATE FUNCTION sd.f1() RETURNS text " \ "LANGUAGE sql SECURITY DEFINER AS $_$%s$_$" % SOURCE1 def test_create_c_lang_function(self): "Create a dynamically loaded C language function" # NOTE 1: Needs contrib/spi module to be available # NOTE 2: Needs superuser privilege inmap = self.std_map() inmap['schema sd'].update({'function autoinc()': { 'language': 'c', 'returns': 'trigger', 'obj_file': '$libdir/autoinc'}}) sql = self.to_sql(inmap) assert fix_indent(sql[0]) == "CREATE FUNCTION sd.autoinc() " \ "RETURNS trigger LANGUAGE c AS '$libdir/autoinc', 'autoinc'" def test_create_function_config(self): "Create a function with a configuration parameter" inmap = self.std_map() inmap['schema sd'].update({'function f1()': { 'language': 'sql', 'returns': 'date', 'configuration': ['DateStyle=postgres, dmy'], 'source': "SELECT CURRENT_DATE"}}) sql = self.to_sql(inmap) assert fix_indent(sql[1]) == "CREATE FUNCTION sd.f1() RETURNS date " \ "LANGUAGE sql SET DateStyle=postgres, dmy AS " \ "$_$SELECT CURRENT_DATE$_$" def test_create_function_in_schema(self): "Create a function within a non-default schema" inmap = self.std_map() inmap.update({'schema s1': {'function f1()': { 'language': 'sql', 'returns': 'text', 'source': SOURCE1, 'volatility': 'immutable'}}}) sql = self.to_sql(inmap, ["CREATE SCHEMA s1"]) assert fix_indent(sql[1]) == "CREATE FUNCTION s1.f1() RETURNS text " \ "LANGUAGE sql IMMUTABLE AS $_$%s$_$" % SOURCE1 def test_bad_function_map(self): "Error creating a function with a bad map" inmap = self.std_map() inmap['schema sd'].update({'f1()': { 'language': 'sql', 'returns': 'text', 'source': SOURCE1}}) with pytest.raises(KeyError): self.to_sql(inmap) def test_drop_function1(self): "Drop an existing function with no arguments" sql = self.to_sql(self.std_map(), [CREATE_STMT1]) assert sql == ["DROP FUNCTION sd.f1()"] def test_drop_function_with_args(self): "Drop an existing function which has arguments" sql = self.to_sql(self.std_map(), [CREATE_STMT2]) assert sql == ["DROP FUNCTION sd.f1(integer, integer)"] def test_change_function_defn(self): "Change function definition" inmap = self.std_map() inmap['schema sd'].update({'function f1()': { 'language': 'sql', 'returns': 'text', 'source': "SELECT 'example'::text", 'volatility': 'immutable'}}) sql = self.to_sql(inmap, [CREATE_STMT1]) assert fix_indent(sql[1]) == "CREATE OR REPLACE FUNCTION sd.f1() " \ "RETURNS text LANGUAGE sql IMMUTABLE AS " \ "$_$SELECT 'example'::text$_$" def test_function_with_comment(self): "Create a function with a comment" inmap = self.std_map() inmap['schema sd'].update({ 'function f1(integer, integer)': { 'description': 'Test function f1', 'language': 'sql', 'returns': 'integer', 'source': SOURCE2}}) sql = self.to_sql(inmap) assert fix_indent(sql[1]) == "CREATE FUNCTION sd.f1(integer, integer)"\ " RETURNS integer LANGUAGE sql AS $_$%s$_$" % SOURCE2 assert sql[2] == COMMENT_STMT def test_comment_on_function(self): "Create a comment for an existing function" inmap = self.std_map() inmap['schema sd'].update({ 'function f1(integer, integer)': { 'description': 'Test function f1', 'language': 'sql', 'returns': 'integer', 'source': SOURCE2}}) sql = self.to_sql(inmap, [CREATE_STMT2]) assert sql == [COMMENT_STMT] def test_drop_function_comment(self): "Drop a comment on an existing function" stmts = [CREATE_STMT2, COMMENT_STMT] inmap = self.std_map() inmap['schema sd'].update({ 'function f1(integer, integer)': { 'language': 'sql', 'returns': 'integer', 'source': SOURCE2}}) sql = self.to_sql(inmap, stmts) assert sql == ["COMMENT ON FUNCTION sd.f1(integer, integer) IS NULL"] def test_change_function_comment(self): "Change existing comment on a function" stmts = [CREATE_STMT2, COMMENT_STMT] inmap = self.std_map() inmap['schema sd'].update({ 'function f1(integer, integer)': { 'description': 'Changed function f1', 'language': 'sql', 'returns': 'integer', 'source': SOURCE2}}) sql = self.to_sql(inmap, stmts) assert sql == ["COMMENT ON FUNCTION sd.f1(integer, integer) IS " "'Changed function f1'"] def test_function_leakproof(self): "Create a function with LEAKPROOF qualifier" inmap = self.std_map() inmap['schema sd'].update({ 'function f1(integer, integer)': { 'language': 'sql', 'returns': 'integer', 'leakproof': True, 'source': SOURCE4, 'volatility': 'immutable'}}) sql = self.to_sql(inmap, superuser=True) assert fix_indent(sql[1]) == "CREATE FUNCTION sd.f1(integer, integer)"\ " RETURNS integer LANGUAGE sql IMMUTABLE LEAKPROOF AS " \ "$_$%s$_$" % SOURCE4 def test_alter_function_leakproof(self): "Change a function with LEAKPROOF qualifier" inmap = self.std_map() inmap['schema sd'].update({ 'function fadd(integer, integer)': { 'language': 'sql', 'returns': 'integer', 'source': SOURCE4, 'volatility': 'immutable'}}) stmt = CREATE_STMT4.replace("IMMUTABLE", "IMMUTABLE LEAKPROOF") sql = self.to_sql(inmap, [stmt], superuser=True) assert fix_indent(sql[0]) == \ "ALTER FUNCTION sd.fadd(integer, integer) NOT LEAKPROOF" def test_change_function_return_type(self): source = lambda rtype: "SELECT '127.0.0.1'::{}".format(rtype) old_type = 'text' new_type = 'inet' statement = lambda rtype: cleandoc(""" CREATE OR REPLACE FUNCTION sd.fget_addr() RETURNS {rtype} LANGUAGE sql IMMUTABLE AS $_${body}$_$""" ).format( rtype=rtype, body=source(rtype), ).replace('\n', ' ') inmap = self.std_map() inmap['schema sd'].update({ 'function fget_addr()': { 'language': 'sql', 'returns': new_type, 'source': source(new_type), } }) sql = self.to_sql(inmap, [statement(old_type)]) assert statement(new_type) == fix_indent(sql[1]) class AggregateToMapTestCase(DatabaseToMapTestCase): """Test mapping of existing aggregates""" def test_map_aggregate_simple(self): "Map a simple aggregate" stmts = [CREATE_STMT2, "CREATE AGGREGATE a1 (integer) (" "SFUNC = f1, STYPE = integer)"] dbmap = self.to_map(stmts) expmap = {'sfunc': 'f1', 'stype': 'integer'} assert dbmap['schema sd']['function f1(integer, integer)'] == \ {'language': 'sql', 'returns': 'integer', 'source': SOURCE2, 'volatility': 'immutable'} assert dbmap['schema sd']['aggregate a1(integer)'] == expmap def test_map_aggregate_init_final(self): "Map an aggregate with an INITCOND and a FINALFUNC" stmts = [CREATE_STMT2, "CREATE FUNCTION f2(integer) RETURNS float " "LANGUAGE sql AS $_$SELECT $1::float$_$ IMMUTABLE", "CREATE AGGREGATE a1 (integer) (SFUNC = f1, STYPE = integer, " "FINALFUNC = f2, INITCOND = '-1')"] dbmap = self.to_map(stmts) expmap = {'sfunc': 'f1', 'stype': 'integer', 'initcond': '-1', 'finalfunc': 'f2'} assert dbmap['schema sd']['function f1(integer, integer)'] == \ {'language': 'sql', 'returns': 'integer', 'source': SOURCE2, 'volatility': 'immutable'} assert dbmap['schema sd']['function f2(integer)'] == \ {'language': 'sql', 'returns': 'double precision', 'source': "SELECT $1::float", 'volatility': 'immutable'} assert dbmap['schema sd']['aggregate a1(integer)'] == expmap def test_map_aggregate_sortop(self): "Map an aggregate with a SORTOP" stmts = [CREATE_STMT2, "CREATE AGGREGATE a1 (integer) (" "SFUNC = f1, STYPE = integer, SORTOP = >)"] dbmap = self.to_map(stmts) expmap = {'sfunc': 'f1', 'stype': 'integer', 'sortop': 'pg_catalog.>'} assert dbmap['schema sd']['aggregate a1(integer)'] == expmap def test_map_moving_aggregate(self): "Map a moving-aggregate mode function" stmts = [CREATE_STMT4, CREATE_STMT5, "CREATE AGGREGATE a1 (integer) (sfunc = fadd, " "stype = integer, initcond = '0', msfunc = fadd, " "minvfunc = fsub, mstype = integer, minitcond = '0')"] dbmap = self.to_map(stmts) expmap = {'sfunc': 'fadd', 'stype': 'integer', 'initcond': '0', 'msfunc': 'fadd', 'minvfunc': 'fsub', 'mstype': 'integer', 'minitcond': '0'} assert dbmap['schema sd']['aggregate a1(integer)'] == expmap def test_map_ordered_set_aggregate(self): "Map an ordered-set aggregate" stmts = [CREATE_STMT2, "CREATE AGGREGATE a1 (integer ORDER BY " "integer) (sfunc = f1, stype = integer)"] dbmap = self.to_map(stmts) expmap = {'sfunc': 'f1', 'stype': 'integer', 'kind': 'ordered'} assert dbmap['schema sd'][ 'aggregate a1(integer ORDER BY integer)'] == expmap def test_map_aggregate_restricted(self): "Map an aggregate with restricted parallel safety" stmts = [CREATE_STMT2, "CREATE AGGREGATE a1 (integer) (" "SFUNC = f1, STYPE = integer, PARALLEL = RESTRICTED)"] dbmap = self.to_map(stmts) expmap = {'sfunc': 'f1', 'stype': 'integer', 'parallel': 'restricted'} assert dbmap['schema sd']['aggregate a1(integer)'] == expmap class AggregateToSqlTestCase(InputMapToSqlTestCase): """Test SQL generation from input aggregates""" def test_create_aggregate_simple(self): "Create a simple aggregate" inmap = self.std_map() inmap['schema sd'].update({'function f1(integer, integer)': { 'language': 'sql', 'returns': 'integer', 'source': SOURCE2, 'volatility': 'immutable'}}) inmap['schema sd'].update({'aggregate a1(integer)': { 'sfunc': 'f1', 'stype': 'integer'}}) sql = self.to_sql(inmap) assert fix_indent(sql[1]) == CREATE_STMT2 assert fix_indent(sql[2]) == "CREATE AGGREGATE sd.a1(integer) " \ "(SFUNC = sd.f1, STYPE = integer)" def test_create_aggregate_sortop(self): "Create an aggregate that specifies a sort operator" inmap = self.std_map() inmap['schema sd'].update({'function f1(float, float)': { 'language': 'sql', 'returns': 'float', 'source': SOURCE2, 'volatility': 'immutable'}}) inmap['schema sd'].update({'aggregate a1(float)': { 'sfunc': 'f1', 'stype': 'float', 'sortop': 'pg_catalog.>'}}) sql = self.to_sql(inmap) assert fix_indent(sql[2]) == "CREATE AGGREGATE sd.a1(float) " \ "(SFUNC = sd.f1, STYPE = float, SORTOP = OPERATOR(pg_catalog.>))" def test_create_aggregate_init_final(self): "Create an aggregate with an INITCOND and a FINALFUNC" inmap = self.std_map() inmap['schema sd'].update({'function f1(integer, integer)': { 'language': 'sql', 'returns': 'integer', 'source': SOURCE2, 'volatility': 'immutable'}}) inmap['schema sd'].update({'function f2(integer)': { 'language': 'sql', 'returns': 'double precision', 'source': "SELECT $1::float", 'volatility': 'immutable'}}) inmap['schema sd'].update({'aggregate a1(integer)': { 'sfunc': 'f1', 'stype': 'integer', 'initcond': '-1', 'finalfunc': 'f2'}}) sql = self.to_sql(inmap) funcs = sorted(sql[1:3]) assert fix_indent(funcs[0]) == CREATE_STMT2 assert fix_indent(funcs[1]) == "CREATE FUNCTION sd.f2(integer) " \ "RETURNS double precision LANGUAGE sql IMMUTABLE " \ "AS $_$SELECT $1::float$_$" assert fix_indent(sql[3]) == "CREATE AGGREGATE sd.a1(integer) " \ "(SFUNC = sd.f1, STYPE = integer, FINALFUNC = sd.f2, " \ "INITCOND = '-1')" def test_drop_aggregate(self): "Drop an existing aggregate" stmts = [CREATE_STMT2, "CREATE AGGREGATE agg1 (integer) " "(SFUNC = f1, STYPE = integer)"] sql = self.to_sql(self.std_map(), stmts) assert sql[0] == "DROP AGGREGATE sd.agg1(integer)" assert sql[1] == "DROP FUNCTION sd.f1(integer, integer)" def test_create_moving_aggregate(self): "Create a moving-aggregate mode function" inmap = self.std_map() inmap['schema sd'].update( {'function fadd(integer, integer)': { 'language': 'sql', 'returns': 'integer', 'source': SOURCE4, 'volatility': 'immutable'}, 'function fsub(integer, integer)': { 'language': 'sql', 'returns': 'integer', 'source': SOURCE5, 'volatility': 'immutable'}, 'aggregate a1(integer)': { 'sfunc': 'fadd', 'stype': 'integer', 'initcond': '0', 'msfunc': 'fadd', 'minvfunc': 'fsub', 'mstype': 'integer', 'minitcond': '0'}}) sql = self.to_sql(inmap, [CREATE_STMT4, CREATE_STMT5]) assert fix_indent(sql[0]) == "CREATE AGGREGATE sd.a1(integer) (" \ "SFUNC = sd.fadd, STYPE = integer, INITCOND = '0', " \ "MSFUNC = sd.fadd, MINVFUNC = sd.fsub, MSTYPE = integer, " \ "MINITCOND = '0')" def test_create_hypothetical_set_aggregate(self): "Create a hypothetical-set aggregate" inmap = self.std_map() inmap['schema sd'].update({ 'function f1(integer, integer)': { 'language': 'sql', 'returns': 'integer', 'source': SOURCE2, 'volatility': 'immutable'}, 'aggregate a1(integer ORDER BY integer)': { 'kind': 'hypothetical', 'sfunc': 'f1', 'stype': 'integer'}}) sql = self.to_sql(inmap, [CREATE_STMT2]) assert fix_indent(sql[0]) == "CREATE AGGREGATE sd.a1(integer " \ "ORDER BY integer) (SFUNC = sd.f1, STYPE = integer, HYPOTHETICAL)" def test_create_aggregate_parallel_safe(self): "Create an aggregate with parallel safety" inmap = self.std_map() inmap['schema sd'].update({ 'function f1(integer, integer)': { 'language': 'sql', 'returns': 'integer', 'source': SOURCE2, 'volatility': 'immutable'}, 'aggregate a1(integer ORDER BY integer)': { 'sfunc': 'f1', 'stype': 'integer', 'parallel': 'safe'}}) sql = self.to_sql(inmap, [CREATE_STMT2]) assert fix_indent(sql[0]) == "CREATE AGGREGATE sd.a1(integer " \ "ORDER BY integer) (SFUNC = sd.f1, STYPE = integer, " \ "PARALLEL = SAFE)"