diff --git a/azure/functions/decorators/constants.py b/azure/functions/decorators/constants.py index c622812e..12d8e5ef 100644 --- a/azure/functions/decorators/constants.py +++ b/azure/functions/decorators/constants.py @@ -19,6 +19,8 @@ EVENT_GRID_TRIGGER = "eventGridTrigger" EVENT_GRID = "eventGrid" TABLE = "table" +SQL = "sql" +SQL_TRIGGER = "sqlTrigger" DAPR_SERVICE_INVOCATION_TRIGGER = "daprServiceInvocationTrigger" DAPR_BINDING_TRIGGER = "daprBindingTrigger" DAPR_TOPIC_TRIGGER = "daprTopicTrigger" diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index e3319ee1..3af406dc 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -23,6 +23,7 @@ from azure.functions.decorators.servicebus import ServiceBusQueueTrigger, \ ServiceBusQueueOutput, ServiceBusTopicTrigger, \ ServiceBusTopicOutput +from azure.functions.decorators.sql import SqlTrigger, SqlInput, SqlOutput from azure.functions.decorators.table import TableInput, TableOutput from azure.functions.decorators.timer import TimerTrigger from azure.functions.decorators.utils import parse_singular_param_to_enum, \ @@ -1069,6 +1070,61 @@ def decorator(): return wrap + def sql_trigger(self, + arg_name: str, + table_name: str, + connection_string_setting: str, + leases_table_name: Optional[str] = None, + data_type: Optional[DataType] = None, + **kwargs) -> Callable[..., Any]: + """The sql_trigger decorator adds :class:`SqlTrigger` + to the :class:`FunctionBuilder` object + for building :class:`Function` object used in worker function + indexing model. This decorator will work only with extension bundle 4.x + and above. + This is equivalent to defining SqlTrigger in the function.json which + enables function to be triggered when there are changes in the Sql + table. + All optional fields will be given default value by function host when + they are parsed by function host. + + Ref: https://aka.ms/sqlbindings + + :param arg_name: The name of the variable that represents a + :class:`SqlRowList` object in the function code + :param table_name: The name of the table monitored by the trigger + :param connection_string_setting: The name of an app setting that + contains the connection string for the database against which the + query or stored procedure is being executed + :param leases_table_name: The name of the table used to store + leases. If not specified, the leases table name will be + Leases_{FunctionId}_{TableId}. + :param data_type: Defines how Functions runtime should treat the + parameter value + :param kwargs: Keyword arguments for specifying additional binding + fields to include in the binding json + + :return: Decorator function. + """ + + @self._configure_function_builder + def wrap(fb): + def decorator(): + fb.add_trigger( + trigger=SqlTrigger( + name=arg_name, + table_name=table_name, + connection_string_setting=connection_string_setting, + leases_table_name=leases_table_name, + data_type=parse_singular_param_to_enum(data_type, + DataType), + **kwargs)) + return fb + + return decorator() + + return wrap + def generic_trigger(self, arg_name: str, type: str, @@ -1859,6 +1915,115 @@ def decorator(): return wrap + def sql_input(self, + arg_name: str, + command_text: str, + connection_string_setting: str, + command_type: Optional[str] = 'Text', + parameters: Optional[str] = None, + data_type: Optional[DataType] = None, + **kwargs) -> Callable[..., Any]: + """The sql_input decorator adds + :class:`SqlInput` to the :class:`FunctionBuilder` object + for building :class:`Function` object used in worker function + indexing model. This decorator will work only with extension bundle 4.x + and above. + This is equivalent to defining SqlInput in the function.json which + enables the function to read from a Sql database. + All optional fields will be given default value by function host when + they are parsed by function host. + + Ref: https://aka.ms/sqlbindings + + :param arg_name: The name of the variable that represents a + :class:`SqlRowList` input object in function code + :param command_text: The Transact-SQL query command or name of the + stored procedure executed by the binding + :param connection_string_setting: The name of an app setting that + contains the connection string for the database against which the + query or stored procedure is being executed + :param command_type: A CommandType value, which is Text for a query + and StoredProcedure for a stored procedure + :param parameters: Zero or more parameter values passed to the + command during execution as a single string. Must follow the format + @param1=param1,@param2=param2 + :param data_type: Defines how Functions runtime should treat the + parameter value + :param kwargs: Keyword arguments for specifying additional binding + fields to include in the binding json + + :return: Decorator function. + """ + + @self._configure_function_builder + def wrap(fb): + def decorator(): + fb.add_binding( + binding=SqlInput( + name=arg_name, + command_text=command_text, + connection_string_setting=connection_string_setting, + command_type=command_type, + parameters=parameters, + data_type=parse_singular_param_to_enum(data_type, + DataType), + **kwargs)) + return fb + + return decorator() + + return wrap + + def sql_output(self, + arg_name: str, + command_text: str, + connection_string_setting: str, + data_type: Optional[DataType] = None, + **kwargs) -> Callable[..., Any]: + """The sql_output decorator adds + :class:`SqlOutput` to the :class:`FunctionBuilder` object + for building :class:`Function` object used in worker function + indexing model. This decorator will work only with extension bundle 4.x + and above. + This is equivalent to defining SqlOutput in the function.json which + enables the function to write to a Sql database. + All optional fields will be given default value by function host when + they are parsed by function host. + + Ref: https://aka.ms/sqlbindings + + :param arg_name: The name of the variable that represents + Sql output object in function code + :param command_text: The Transact-SQL query command or name of the + stored procedure executed by the binding + :param connection_string_setting: The name of an app setting that + contains the connection string for the database against which the + query or stored procedure is being executed + :param data_type: Defines how Functions runtime should treat the + parameter value + :param kwargs: Keyword arguments for specifying additional binding + fields to include in the binding json + + :return: Decorator function. + """ + + @self._configure_function_builder + def wrap(fb): + def decorator(): + fb.add_binding( + binding=SqlOutput( + name=arg_name, + command_text=command_text, + connection_string_setting=connection_string_setting, + data_type=parse_singular_param_to_enum(data_type, + DataType), + **kwargs)) + return fb + + return decorator() + + return wrap + def generic_input_binding(self, arg_name: str, type: str, diff --git a/azure/functions/decorators/sql.py b/azure/functions/decorators/sql.py new file mode 100644 index 00000000..be4b79da --- /dev/null +++ b/azure/functions/decorators/sql.py @@ -0,0 +1,61 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from typing import Optional + +from azure.functions.decorators.constants import SQL, SQL_TRIGGER +from azure.functions.decorators.core import DataType, InputBinding, \ + OutputBinding, Trigger + + +class SqlInput(InputBinding): + @staticmethod + def get_binding_name() -> str: + return SQL + + def __init__(self, + name: str, + command_text: str, + connection_string_setting: str, + command_type: Optional[str] = 'Text', + parameters: Optional[str] = None, + data_type: Optional[DataType] = None, + **kwargs): + self.command_text = command_text + self.connection_string_setting = connection_string_setting + self.command_type = command_type + self.parameters = parameters + super().__init__(name=name, data_type=data_type) + + +class SqlOutput(OutputBinding): + @staticmethod + def get_binding_name() -> str: + return SQL + + def __init__(self, + name: str, + command_text: str, + connection_string_setting: str, + data_type: Optional[DataType] = None, + **kwargs): + self.command_text = command_text + self.connection_string_setting = connection_string_setting + super().__init__(name=name, data_type=data_type) + + +class SqlTrigger(Trigger): + @staticmethod + def get_binding_name() -> str: + return SQL_TRIGGER + + def __init__(self, + name: str, + table_name: str, + connection_string_setting: str, + leases_table_name: Optional[str] = None, + data_type: Optional[DataType] = None, + **kwargs): + self.table_name = table_name + self.connection_string_setting = connection_string_setting + self.leases_table_name = leases_table_name + super().__init__(name=name, data_type=data_type) diff --git a/docs/ProgModelSpec.pyi b/docs/ProgModelSpec.pyi index 857520c1..a01d8168 100644 --- a/docs/ProgModelSpec.pyi +++ b/docs/ProgModelSpec.pyi @@ -548,7 +548,46 @@ class TriggerApi(DecoratorApi, ABC): """ pass - + + def sql_trigger(self, + arg_name: str, + table_name: str, + connection_string_setting: str, + leases_table_name: Optional[str] = None, + data_type: Optional[DataType] = None, + **kwargs) -> Callable[..., Any]: + """The sql_trigger decorator adds :class:`SqlTrigger` + to the :class:`FunctionBuilder` object + for building :class:`Function` object used in worker function + indexing model. This decorator will work only with extension bundle 4.x + and above. + This is equivalent to defining SqlTrigger in the function.json which + enables function to be triggered when there are changes in the Sql + table. + All optional fields will be given default value by function host when + they are parsed by function host. + + Ref: https://aka.ms/sqlbindings + + :param arg_name: The name of the variable that represents a + :class:`SqlRowList` object in the function code + :param table_name: The name of the table monitored by the trigger + :param connection_string_setting: The name of an app setting that + contains the connection string for the database against which the + query or stored procedure is being executed + :param leases_table_name: The name of the table used to store + leases. If not specified, the leases table name will be + Leases_{FunctionId}_{TableId}. + :param data_type: Defines how Functions runtime should treat the + parameter value + :param kwargs: Keyword arguments for specifying additional binding + fields to include in the binding json + + :return: Decorator function. + """ + + pass + def generic_trigger(self, arg_name: str, type: str, @@ -989,6 +1028,83 @@ class BindingApi(DecoratorApi, ABC): pass + def sql_input(self, + arg_name: str, + command_text: str, + connection_string_setting: str, + command_type: Optional[str] = 'Text', + parameters: Optional[str] = None, + data_type: Optional[DataType] = None, + **kwargs) -> Callable[..., Any]: + """The sql_input decorator adds + :class:`SqlInput` to the :class:`FunctionBuilder` object + for building :class:`Function` object used in worker function + indexing model. This decorator will work only with extension bundle 4.x + and above. + This is equivalent to defining SqlInput in the function.json which + enables the function to read from a Sql database. + All optional fields will be given default value by function host when + they are parsed by function host. + + Ref: https://aka.ms/sqlbindings + + :param arg_name: The name of the variable that represents a + :class:`SqlRowList` input object in function code + :param command_text: The Transact-SQL query command or name of the + stored procedure executed by the binding + :param connection_string_setting: The name of an app setting that + contains the connection string for the database against which the + query or stored procedure is being executed + :param command_type: A CommandType value, which is Text for a query + and StoredProcedure for a stored procedure + :param parameters: Zero or more parameter values passed to the + command during execution as a single string. Must follow the format + @param1=param1,@param2=param2 + :param data_type: Defines how Functions runtime should treat the + parameter value + :param kwargs: Keyword arguments for specifying additional binding + fields to include in the binding json + + :return: Decorator function. + """ + + pass + + def sql_output(self, + arg_name: str, + command_text: str, + connection_string_setting: str, + data_type: Optional[DataType] = None, + **kwargs) -> Callable[..., Any]: + """The sql_output decorator adds + :class:`SqlOutput` to the :class:`FunctionBuilder` object + for building :class:`Function` object used in worker function + indexing model. This decorator will work only with extension bundle 4.x + and above. + This is equivalent to defining SqlOutput in the function.json which + enables the function to write to a Sql database. + All optional fields will be given default value by function host when + they are parsed by function host. + + Ref: https://aka.ms/sqlbindings + + :param arg_name: The name of the variable that represents + Sql output object in function code + :param command_text: The Transact-SQL query command or name of the + stored procedure executed by the binding + :param connection_string_setting: The name of an app setting that + contains the connection string for the database against which the + query or stored procedure is being executed + :param data_type: Defines how Functions runtime should treat the + parameter value + :param kwargs: Keyword arguments for specifying additional binding + fields to include in the binding json + + :return: Decorator function. + """ + + pass + def generic_input_binding(self, arg_name: str, type: str, diff --git a/tests/decorators/test_decorators.py b/tests/decorators/test_decorators.py index bb09fda4..d06fd3ae 100644 --- a/tests/decorators/test_decorators.py +++ b/tests/decorators/test_decorators.py @@ -5,7 +5,8 @@ from azure.functions.decorators.constants import TIMER_TRIGGER, HTTP_TRIGGER, \ HTTP_OUTPUT, QUEUE, QUEUE_TRIGGER, SERVICE_BUS, SERVICE_BUS_TRIGGER, \ EVENT_HUB, EVENT_HUB_TRIGGER, COSMOS_DB, COSMOS_DB_TRIGGER, BLOB, \ - BLOB_TRIGGER, EVENT_GRID_TRIGGER, EVENT_GRID, TABLE, WARMUP_TRIGGER + BLOB_TRIGGER, EVENT_GRID_TRIGGER, EVENT_GRID, TABLE, WARMUP_TRIGGER, \ + SQL, SQL_TRIGGER from azure.functions.decorators.core import DataType, AuthLevel, \ BindingDirection, AccessRights, Cardinality from azure.functions.decorators.function_app import FunctionApp @@ -2054,6 +2055,184 @@ def dummy(): "connection": "dummy_out_conn" }) + def test_sql_default_args(self): + app = self.func_app + + @app.sql_trigger(arg_name="trigger", + table_name="dummy_table", + connection_string_setting="dummy_setting") + @app.sql_input(arg_name="in", + command_text="dummy_query", + connection_string_setting="dummy_setting") + @app.sql_output(arg_name="out", + command_text="dummy_table", + connection_string_setting="dummy_setting") + def dummy(): + pass + + func = self._get_user_function(app) + + assert_json(self, func, { + "scriptFile": "function_app.py", + "bindings": [ + { + "direction": BindingDirection.OUT, + "type": SQL, + "name": "out", + "commandText": "dummy_table", + "connectionStringSetting": "dummy_setting" + }, + { + "direction": BindingDirection.IN, + "type": SQL, + "name": "in", + "commandText": "dummy_query", + "connectionStringSetting": "dummy_setting", + "commandType": "Text" + }, + { + "direction": BindingDirection.IN, + "type": SQL_TRIGGER, + "name": "trigger", + "tableName": "dummy_table", + "connectionStringSetting": "dummy_setting" + } + ] + }) + + def test_sql_full_args(self): + app = self.func_app + + @app.sql_trigger(arg_name="trigger", + table_name="dummy_table", + connection_string_setting="dummy_setting", + data_type=DataType.STRING, + dummy_field="dummy") + @app.sql_input(arg_name="in", + command_text="dummy_query", + connection_string_setting="dummy_setting", + command_type="Text", + parameters="dummy_parameters", + data_type=DataType.STRING, + dummy_field="dummy") + @app.sql_output(arg_name="out", + command_text="dummy_table", + connection_string_setting="dummy_setting", + data_type=DataType.STRING, + dummy_field="dummy") + def dummy(): + pass + + func = self._get_user_function(app) + + assert_json(self, func, { + "scriptFile": "function_app.py", + "bindings": [ + { + "direction": BindingDirection.OUT, + 'dummyField': 'dummy', + "dataType": DataType.STRING, + "type": SQL, + "name": "out", + "commandText": "dummy_table", + "connectionStringSetting": "dummy_setting" + }, + { + "direction": BindingDirection.IN, + 'dummyField': 'dummy', + "dataType": DataType.STRING, + "type": SQL, + "name": "in", + "commandText": "dummy_query", + "connectionStringSetting": "dummy_setting", + "parameters": "dummy_parameters", + "commandType": "Text" + }, + { + "direction": BindingDirection.IN, + 'dummyField': 'dummy', + "dataType": DataType.STRING, + "type": SQL_TRIGGER, + "name": "trigger", + "tableName": "dummy_table", + "connectionStringSetting": "dummy_setting" + } + ] + }) + + def test_sql_trigger(self): + app = self.func_app + + @app.sql_trigger(arg_name="trigger", + table_name="dummy_table", + connection_string_setting="dummy_setting") + def dummy(): + pass + + func = self._get_user_function(app) + + self.assertEqual(len(func.get_bindings()), 1) + + output = func.get_bindings()[0] + self.assertEqual(output.get_dict_repr(), { + "direction": BindingDirection.IN, + "type": SQL_TRIGGER, + "name": "trigger", + "tableName": "dummy_table", + "connectionStringSetting": "dummy_setting" + }) + + def test_sql_input_binding(self): + app = self.func_app + + @app.sql_trigger(arg_name="trigger", + table_name="dummy_table", + connection_string_setting="dummy_setting") + @app.sql_input(arg_name="in", + command_text="dummy_query", + connection_string_setting="dummy_setting") + def dummy(): + pass + + func = self._get_user_function(app) + + self.assertEqual(len(func.get_bindings()), 2) + + output = func.get_bindings()[0] + self.assertEqual(output.get_dict_repr(), { + "direction": BindingDirection.IN, + "type": SQL, + "name": "in", + "commandText": "dummy_query", + "connectionStringSetting": "dummy_setting", + "commandType": "Text" + }) + + def test_sql_output_binding(self): + app = self.func_app + + @app.sql_trigger(arg_name="trigger", + table_name="dummy_table", + connection_string_setting="dummy_setting") + @app.sql_output(arg_name="out", + command_text="dummy_table", + connection_string_setting="dummy_setting") + def dummy(): + pass + + func = self._get_user_function(app) + + self.assertEqual(len(func.get_bindings()), 2) + + output = func.get_bindings()[0] + self.assertEqual(output.get_dict_repr(), { + "direction": BindingDirection.OUT, + "type": SQL, + "name": "out", + "commandText": "dummy_table", + "connectionStringSetting": "dummy_setting", + }) + def test_function_app_full_bindings_metadata_key_order(self): app = self.func_app diff --git a/tests/decorators/test_sql.py b/tests/decorators/test_sql.py new file mode 100644 index 00000000..7fbac8e4 --- /dev/null +++ b/tests/decorators/test_sql.py @@ -0,0 +1,60 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import unittest + +from azure.functions.decorators.constants import SQL_TRIGGER, SQL +from azure.functions.decorators.core import BindingDirection, DataType +from azure.functions.decorators.sql import SqlTrigger, \ + SqlInput, SqlOutput + + +class TestSql(unittest.TestCase): + def test_sql_trigger_valid_creation(self): + trigger = SqlTrigger(name="req", + table_name="dummy_table", + connection_string_setting="dummy_setting", + data_type=DataType.UNDEFINED, + dummy_field="dummy") + + self.assertEqual(trigger.get_binding_name(), "sqlTrigger") + self.assertEqual(trigger.get_dict_repr(), + {"connectionStringSetting": "dummy_setting", + "dataType": DataType.UNDEFINED, + "tableName": "dummy_table", + "direction": BindingDirection.IN, + "dummyField": "dummy", + "name": "req", + "type": SQL_TRIGGER}) + + def test_sql_output_valid_creation(self): + output = SqlOutput(name="req", + command_text="dummy_table", + connection_string_setting="dummy_setting", + data_type=DataType.UNDEFINED, + dummy_field="dummy") + self.assertEqual(output.get_binding_name(), "sql") + self.assertEqual(output.get_dict_repr(), + {"commandText": "dummy_table", + "connectionStringSetting": "dummy_setting", + "dataType": DataType.UNDEFINED, + "direction": BindingDirection.OUT, + "dummyField": "dummy", + "name": "req", + "type": SQL}) + + def test_sql_input_valid_creation(self): + input = SqlInput(name="req", + command_text="dummy_query", + connection_string_setting="dummy_setting", + data_type=DataType.UNDEFINED, + dummy_field="dummy") + self.assertEqual(input.get_binding_name(), "sql") + self.assertEqual(input.get_dict_repr(), + {"commandText": "dummy_query", + "connectionStringSetting": "dummy_setting", + "commandType": "Text", + "dataType": DataType.UNDEFINED, + "direction": BindingDirection.IN, + "dummyField": "dummy", + "name": "req", + "type": SQL})