From b432c31aca1c5d1ee4285ff733f7c4fef032ccc8 Mon Sep 17 00:00:00 2001 From: Lucy Zhang Date: Fri, 21 Jul 2023 10:42:36 -0700 Subject: [PATCH 01/11] addLeasesTableNameSetting to SqlTriggerAttribute --- .../src/SqlTriggerAttribute.cs | 12 +++++++++- .../ProductsTriggerWithLeasesTableName.cs | 23 +++++++++++++++++++ samples/samples-csharp/local.settings.json | 3 ++- src/SqlBindingUtilities.cs | 21 +++++++++++++++++ src/TriggerBinding/SqlTriggerAttribute.cs | 12 +++++++++- src/TriggerBinding/SqlTriggerBinding.cs | 7 ++++-- .../SqlTriggerBindingProvider.cs | 5 ++-- src/TriggerBinding/SqlTriggerConstants.cs | 2 ++ src/TriggerBinding/SqlTriggerListener.cs | 7 ++++-- .../SqlTriggerBindingIntegrationTests.cs | 4 ++-- .../SqlTriggerScaleMonitorTests.cs | 2 +- 11 files changed, 86 insertions(+), 12 deletions(-) create mode 100644 samples/samples-csharp/TriggerBindingSamples/ProductsTriggerWithLeasesTableName.cs diff --git a/Worker.Extensions.Sql/src/SqlTriggerAttribute.cs b/Worker.Extensions.Sql/src/SqlTriggerAttribute.cs index db9549ca1..698d74273 100644 --- a/Worker.Extensions.Sql/src/SqlTriggerAttribute.cs +++ b/Worker.Extensions.Sql/src/SqlTriggerAttribute.cs @@ -13,10 +13,12 @@ public sealed class SqlTriggerAttribute : TriggerBindingAttribute /// /// Name of the table to watch for changes. /// The name of the app setting where the SQL connection string is stored - public SqlTriggerAttribute(string tableName, string connectionStringSetting) + /// The name of the app setting where the leases table name is stored + public SqlTriggerAttribute(string tableName, string connectionStringSetting, string leasesTableNameSetting = null) { this.TableName = tableName ?? throw new ArgumentNullException(nameof(tableName)); this.ConnectionStringSetting = connectionStringSetting ?? throw new ArgumentNullException(nameof(connectionStringSetting)); + this.LeasesTableNameSetting = leasesTableNameSetting; } /// @@ -28,5 +30,13 @@ public SqlTriggerAttribute(string tableName, string connectionStringSetting) /// Name of the table to watch for changes. /// public string TableName { get; } + + /// + /// Name of the app setting containing the leases table name. + /// If not specified, the leases table name will be Leases_{FunctionId}_{TableId} + /// More information on how this is generated can be found here + /// https://github.com/Azure/azure-functions-sql-extension/blob/release/trigger/docs/TriggerBinding.md#az_funcleases_ + /// + public string LeasesTableNameSetting { get; } } } \ No newline at end of file diff --git a/samples/samples-csharp/TriggerBindingSamples/ProductsTriggerWithLeasesTableName.cs b/samples/samples-csharp/TriggerBindingSamples/ProductsTriggerWithLeasesTableName.cs new file mode 100644 index 000000000..5dfc7cf6a --- /dev/null +++ b/samples/samples-csharp/TriggerBindingSamples/ProductsTriggerWithLeasesTableName.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.Azure.WebJobs.Extensions.Sql.Samples.Common; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; + +namespace Microsoft.Azure.WebJobs.Extensions.Sql.Samples.TriggerBindingSamples +{ + public static class ProductsTriggerWithLeasesTableName + { + [FunctionName(nameof(ProductsTriggerWithLeasesTableName))] + public static void Run( + [SqlTrigger("[dbo].[Products]", "SqlConnectionString", "LeasesTableName")] + IReadOnlyList> changes, + ILogger logger) + { + // The output is used to inspect the trigger binding parameter in test methods. + logger.LogInformation("SQL Changes: " + JsonConvert.SerializeObject(changes)); + } + } +} diff --git a/samples/samples-csharp/local.settings.json b/samples/samples-csharp/local.settings.json index 2ee8fdfec..6d3f9154b 100644 --- a/samples/samples-csharp/local.settings.json +++ b/samples/samples-csharp/local.settings.json @@ -5,6 +5,7 @@ "FUNCTIONS_WORKER_RUNTIME": "dotnet", "SqlConnectionString": "", "Sp_SelectCost": "SelectProductsCost", - "ProductCost": 100 + "ProductCost": 100, + "LeasesTableName": "leases" } } \ No newline at end of file diff --git a/src/SqlBindingUtilities.cs b/src/SqlBindingUtilities.cs index f2c304354..72d1e7879 100644 --- a/src/SqlBindingUtilities.cs +++ b/src/SqlBindingUtilities.cs @@ -4,12 +4,14 @@ using System; using System.Collections.Generic; using System.Data; +using System.Globalization; using System.Linq; using System.Threading.Tasks; using System.Threading; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using static Microsoft.Azure.WebJobs.Extensions.Sql.SqlTriggerConstants; using static Microsoft.Azure.WebJobs.Extensions.Sql.Telemetry.Telemetry; using Microsoft.Azure.WebJobs.Extensions.Sql.Telemetry; @@ -54,6 +56,25 @@ public static string GetConnectionString(string connectionStringSetting, IConfig return connectionString; } + public static string GetLeasesTableName(string leasesTableNameSetting, IConfiguration configuration) + { + if (string.IsNullOrEmpty(leasesTableNameSetting)) + { + return ""; + } + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + string leasesTableName = configuration.GetConnectionStringOrSetting(leasesTableNameSetting); + if (string.IsNullOrEmpty(leasesTableName)) + { + throw new ArgumentException(leasesTableName == null ? $"LeasesTableNameSetting '{leasesTableNameSetting}' is missing in your function app settings, please add the setting with a leases table name." : + $"LeasesTableNameSetting '{leasesTableNameSetting}' is empty in your function app settings, please update the setting with a leases table name."); + } + return string.Format(CultureInfo.InvariantCulture, UserDefinedLeasesTableNameFormat, $"{leasesTableName}"); + } + /// /// Parses the parameter string into a list of parameters, where each parameter is separated by "," and has the form /// "@param1=param2". "@param1" is the parameter name to be used in the query or stored procedure, and param1 is the diff --git a/src/TriggerBinding/SqlTriggerAttribute.cs b/src/TriggerBinding/SqlTriggerAttribute.cs index 92e8ed790..6fa6f5dc8 100644 --- a/src/TriggerBinding/SqlTriggerAttribute.cs +++ b/src/TriggerBinding/SqlTriggerAttribute.cs @@ -18,10 +18,12 @@ public sealed class SqlTriggerAttribute : Attribute /// /// Name of the table to watch for changes. /// The name of the app setting where the SQL connection string is stored - public SqlTriggerAttribute(string tableName, string connectionStringSetting) + /// The name of the app setting where the leases table name is stored + public SqlTriggerAttribute(string tableName, string connectionStringSetting, string leasesTableNameSetting = null) { this.TableName = tableName ?? throw new ArgumentNullException(nameof(tableName)); this.ConnectionStringSetting = connectionStringSetting ?? throw new ArgumentNullException(nameof(connectionStringSetting)); + this.LeasesTableNameSetting = leasesTableNameSetting; } /// @@ -34,5 +36,13 @@ public SqlTriggerAttribute(string tableName, string connectionStringSetting) /// Name of the table to watch for changes. /// public string TableName { get; } + + /// + /// Name of the app setting containing the leases table name. + /// If not specified, the leases table name will be Leases_{FunctionId}_{TableId} + /// More information on how this is generated can be found here + /// https://github.com/Azure/azure-functions-sql-extension/blob/release/trigger/docs/TriggerBinding.md#az_funcleases_ + /// + public string LeasesTableNameSetting { get; } } } \ No newline at end of file diff --git a/src/TriggerBinding/SqlTriggerBinding.cs b/src/TriggerBinding/SqlTriggerBinding.cs index 841c8a79e..4e8415de1 100644 --- a/src/TriggerBinding/SqlTriggerBinding.cs +++ b/src/TriggerBinding/SqlTriggerBinding.cs @@ -27,6 +27,7 @@ internal sealed class SqlTriggerBinding : ITriggerBinding { private readonly string _connectionString; private readonly string _tableName; + private readonly string _leasesTableName; private readonly ParameterInfo _parameter; private readonly IHostIdProvider _hostIdProvider; private readonly ILogger _logger; @@ -40,14 +41,16 @@ internal sealed class SqlTriggerBinding : ITriggerBinding /// /// SQL connection string used to connect to user database /// Name of the user table + /// Optional - Name of the leases table /// Trigger binding parameter information /// Provider of unique host identifier /// Facilitates logging of messages /// Provides configuration values - public SqlTriggerBinding(string connectionString, string tableName, ParameterInfo parameter, IHostIdProvider hostIdProvider, ILogger logger, IConfiguration configuration) + public SqlTriggerBinding(string connectionString, string tableName, string leasesTableName, ParameterInfo parameter, IHostIdProvider hostIdProvider, ILogger logger, IConfiguration configuration) { this._connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString)); this._tableName = tableName ?? throw new ArgumentNullException(nameof(tableName)); + this._leasesTableName = leasesTableName; this._parameter = parameter ?? throw new ArgumentNullException(nameof(parameter)); this._hostIdProvider = hostIdProvider ?? throw new ArgumentNullException(nameof(hostIdProvider)); this._logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -72,7 +75,7 @@ public async Task CreateListenerAsync(ListenerFactoryContext context) _ = context ?? throw new ArgumentNullException(nameof(context), "Missing listener context"); string userFunctionId = await this.GetUserFunctionIdAsync(); - return new SqlTriggerListener(this._connectionString, this._tableName, userFunctionId, context.Executor, this._logger, this._configuration); + return new SqlTriggerListener(this._connectionString, this._tableName, this._leasesTableName, userFunctionId, context.Executor, this._logger, this._configuration); } public ParameterDescriptor ToParameterDescriptor() diff --git a/src/TriggerBinding/SqlTriggerBindingProvider.cs b/src/TriggerBinding/SqlTriggerBindingProvider.cs index 5c139c299..fa4f86315 100644 --- a/src/TriggerBinding/SqlTriggerBindingProvider.cs +++ b/src/TriggerBinding/SqlTriggerBindingProvider.cs @@ -73,6 +73,7 @@ public Task TryCreateAsync(TriggerBindingProviderContext contex } string connectionString = SqlBindingUtilities.GetConnectionString(attribute.ConnectionStringSetting, this._configuration); + string leasesTableName = SqlBindingUtilities.GetLeasesTableName(attribute.LeasesTableNameSetting, this._configuration); Type bindingType; // Instantiate class 'SqlTriggerBinding' for non .NET In-Proc functions. @@ -87,10 +88,10 @@ public Task TryCreateAsync(TriggerBindingProviderContext contex bindingType = typeof(SqlTriggerBinding<>).MakeGenericType(userType); } - var constructorParameterTypes = new Type[] { typeof(string), typeof(string), typeof(ParameterInfo), typeof(IHostIdProvider), typeof(ILogger), typeof(IConfiguration) }; + var constructorParameterTypes = new Type[] { typeof(string), typeof(string), typeof(string), typeof(ParameterInfo), typeof(IHostIdProvider), typeof(ILogger), typeof(IConfiguration) }; ConstructorInfo bindingConstructor = bindingType.GetConstructor(constructorParameterTypes); - object[] constructorParameterValues = new object[] { connectionString, attribute.TableName, parameter, this._hostIdProvider, this._logger, this._configuration }; + object[] constructorParameterValues = new object[] { connectionString, attribute.TableName, leasesTableName, parameter, this._hostIdProvider, this._logger, this._configuration }; var triggerBinding = (ITriggerBinding)bindingConstructor.Invoke(constructorParameterValues); return Task.FromResult(triggerBinding); diff --git a/src/TriggerBinding/SqlTriggerConstants.cs b/src/TriggerBinding/SqlTriggerConstants.cs index 16fbcdf3e..6f360e101 100644 --- a/src/TriggerBinding/SqlTriggerConstants.cs +++ b/src/TriggerBinding/SqlTriggerConstants.cs @@ -11,6 +11,8 @@ internal static class SqlTriggerConstants public const string LeasesTableNameFormat = "[" + SchemaName + "].[Leases_{0}]"; + public const string UserDefinedLeasesTableNameFormat = "[" + SchemaName + "].[{0}]"; + public const string LeasesTableChangeVersionColumnName = "_az_func_ChangeVersion"; public const string LeasesTableAttemptCountColumnName = "_az_func_AttemptCount"; public const string LeasesTableLeaseExpirationTimeColumnName = "_az_func_LeaseExpirationTime"; diff --git a/src/TriggerBinding/SqlTriggerListener.cs b/src/TriggerBinding/SqlTriggerListener.cs index bf578ff60..dc5953ac1 100644 --- a/src/TriggerBinding/SqlTriggerListener.cs +++ b/src/TriggerBinding/SqlTriggerListener.cs @@ -40,6 +40,7 @@ internal sealed class SqlTriggerListener : IListener, IScaleMonitorProvider, private readonly SqlObject _userTable; private readonly string _connectionString; + private readonly string _userDefinedLeasesTableName; private readonly string _userFunctionId; private readonly ITriggeredFunctionExecutor _executor; private readonly ILogger _logger; @@ -61,14 +62,16 @@ internal sealed class SqlTriggerListener : IListener, IScaleMonitorProvider, /// /// SQL connection string used to connect to user database /// Name of the user table + /// Optional - Name of the leases table /// Unique identifier for the user function /// Defines contract for triggering user function /// Facilitates logging of messages /// Provides configuration values - public SqlTriggerListener(string connectionString, string tableName, string userFunctionId, ITriggeredFunctionExecutor executor, ILogger logger, IConfiguration configuration) + public SqlTriggerListener(string connectionString, string tableName, string leasesTableName, string userFunctionId, ITriggeredFunctionExecutor executor, ILogger logger, IConfiguration configuration) { this._connectionString = !string.IsNullOrEmpty(connectionString) ? connectionString : throw new ArgumentNullException(nameof(connectionString)); this._userTable = !string.IsNullOrEmpty(tableName) ? new SqlObject(tableName) : throw new ArgumentNullException(nameof(tableName)); + this._userDefinedLeasesTableName = leasesTableName; this._userFunctionId = !string.IsNullOrEmpty(userFunctionId) ? userFunctionId : throw new ArgumentNullException(nameof(userFunctionId)); this._executor = executor ?? throw new ArgumentNullException(nameof(executor)); this._logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -123,7 +126,7 @@ public async Task StartAsync(CancellationToken cancellationToken) IReadOnlyList<(string name, string type)> primaryKeyColumns = await GetPrimaryKeyColumnsAsync(connection, userTableId, this._logger, this._userTable.FullName, cancellationToken); IReadOnlyList userTableColumns = await this.GetUserTableColumnsAsync(connection, userTableId, cancellationToken); - string leasesTableName = string.Format(CultureInfo.InvariantCulture, LeasesTableNameFormat, $"{this._userFunctionId}_{userTableId}"); + string leasesTableName = this._userDefinedLeasesTableName ?? string.Format(CultureInfo.InvariantCulture, LeasesTableNameFormat, $"{this._userFunctionId}_{userTableId}"); this._telemetryProps[TelemetryPropertyName.LeasesTableName] = leasesTableName; var transactionSw = Stopwatch.StartNew(); diff --git a/test/Integration/SqlTriggerBindingIntegrationTests.cs b/test/Integration/SqlTriggerBindingIntegrationTests.cs index f5f93b26d..343891c9f 100644 --- a/test/Integration/SqlTriggerBindingIntegrationTests.cs +++ b/test/Integration/SqlTriggerBindingIntegrationTests.cs @@ -544,7 +544,7 @@ public async void GetMetricsTest() this.SetChangeTrackingForTable("Products"); string userFunctionId = "func-id"; IConfiguration configuration = new ConfigurationBuilder().Build(); - var listener = new SqlTriggerListener(this.DbConnectionString, "dbo.Products", userFunctionId, Mock.Of(), Mock.Of(), configuration); + var listener = new SqlTriggerListener(this.DbConnectionString, "dbo.Products", "", userFunctionId, Mock.Of(), Mock.Of(), configuration); await listener.StartAsync(CancellationToken.None); // Cancel immediately so the listener doesn't start processing the changes await listener.StopAsync(CancellationToken.None); @@ -625,7 +625,7 @@ public async void LastAccessTimeColumn_Created_OnStartup() this.SetChangeTrackingForTable("Products"); string userFunctionId = "func-id"; IConfiguration configuration = new ConfigurationBuilder().Build(); - var listener = new SqlTriggerListener(this.DbConnectionString, "dbo.Products", userFunctionId, Mock.Of(), Mock.Of(), configuration); + var listener = new SqlTriggerListener(this.DbConnectionString, "dbo.Products", "", userFunctionId, Mock.Of(), Mock.Of(), configuration); await listener.StartAsync(CancellationToken.None); // Cancel immediately so the listener doesn't start processing the changes await listener.StopAsync(CancellationToken.None); diff --git a/test/Unit/TriggerBinding/SqlTriggerScaleMonitorTests.cs b/test/Unit/TriggerBinding/SqlTriggerScaleMonitorTests.cs index b7d5db856..8bf093f7f 100644 --- a/test/Unit/TriggerBinding/SqlTriggerScaleMonitorTests.cs +++ b/test/Unit/TriggerBinding/SqlTriggerScaleMonitorTests.cs @@ -226,7 +226,7 @@ public void InvalidUserConfiguredMaxChangesPerWorker(string maxChangesPerWorker) (Mock mockLogger, List logMessages) = CreateMockLogger(); Mock mockConfiguration = CreateMockConfiguration(maxChangesPerWorker); - Assert.Throws(() => new SqlTriggerListener("testConnectionString", "testTableName", "testUserFunctionId", Mock.Of(), mockLogger.Object, mockConfiguration.Object)); + Assert.Throws(() => new SqlTriggerListener("testConnectionString", "testTableName", "", "testUserFunctionId", Mock.Of(), mockLogger.Object, mockConfiguration.Object)); } private static IScaleMonitor GetScaleMonitor(string tableName, string userFunctionId) From e30538efcc756da76e9a3c33865b67b92fee9443 Mon Sep 17 00:00:00 2001 From: Lucy Zhang Date: Fri, 21 Jul 2023 11:40:33 -0700 Subject: [PATCH 02/11] add provider test --- .../SqlTriggerBindingProviderTests.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/test/Unit/TriggerBinding/SqlTriggerBindingProviderTests.cs b/test/Unit/TriggerBinding/SqlTriggerBindingProviderTests.cs index 727a5083a..29c2d4926 100644 --- a/test/Unit/TriggerBinding/SqlTriggerBindingProviderTests.cs +++ b/test/Unit/TriggerBinding/SqlTriggerBindingProviderTests.cs @@ -78,10 +78,22 @@ public async Task TryCreateAsync_ValidTriggerParameterType_ReturnsTriggerBinding Assert.IsType>(binding); } + /// + /// Verifies that is returned if the has all + /// required and optional properties set and it is applied on the trigger parameter of supported type. + /// + [Fact] + public async Task TryCreateAsync_TableName_ReturnsTriggerBinding() + { + Type parameterType = typeof(IReadOnlyList>); + ITriggerBinding binding = await CreateTriggerBindingAsync(parameterType, nameof(UserFunctionWithTableName)); + Assert.IsType>(binding); + } + private static async Task CreateTriggerBindingAsync(Type parameterType, string methodName) { var provider = new SqlTriggerBindingProvider( - Mock.Of(c => c["testConnectionStringSetting"] == "testConnectionString"), + Mock.Of(c => c["testConnectionStringSetting"] == "testConnectionString" && c["testLeasesTableNameSetting"] == "testLeasesTableName"), Mock.Of(), Mock.Of(f => f.CreateLogger(It.IsAny()) == Mock.Of())); @@ -99,5 +111,7 @@ private static void UserFunctionWithoutAttribute(T _) { } private static void UserFunctionWithoutConnectionString([SqlTrigger("testTableName", null)] T _) { } private static void UserFunctionWithAttribute([SqlTrigger("testTableName", "testConnectionStringSetting")] T _) { } + + private static void UserFunctionWithTableName([SqlTrigger("testTableName", "testConnectionStringSetting", "testLeasesTableNameSetting")] T _) { } } } \ No newline at end of file From 97a6f2793eed375c9a91781d74173cded03a8208 Mon Sep 17 00:00:00 2001 From: Lucy Zhang Date: Fri, 21 Jul 2023 11:51:47 -0700 Subject: [PATCH 03/11] add samples --- samples/samples-csharp/local.settings.json | 2 +- .../function.json | 13 ++++++++ .../ProductsTriggerLeasesTableName/run.csx | 14 +++++++++ samples/samples-csx/local.settings.json | 3 +- .../function.json | 13 ++++++++ .../ProductsTriggerLeasesTableName/index.js | 6 ++++ samples/samples-js/local.settings.json | 3 +- .../ProductsTriggerWithLeasesTableName.cs | 30 +++++++++++++++++++ samples/samples-outofproc/local.settings.json | 3 +- .../function.json | 13 ++++++++ .../ProductsTriggerLeasesTableName/run.ps1 | 10 +++++++ .../samples-powershell/local.settings.json | 3 +- .../__init__.py | 8 +++++ .../function.json | 13 ++++++++ samples/samples-python/local.settings.json | 3 +- 15 files changed, 131 insertions(+), 6 deletions(-) create mode 100644 samples/samples-csx/ProductsTriggerLeasesTableName/function.json create mode 100644 samples/samples-csx/ProductsTriggerLeasesTableName/run.csx create mode 100644 samples/samples-js/ProductsTriggerLeasesTableName/function.json create mode 100644 samples/samples-js/ProductsTriggerLeasesTableName/index.js create mode 100644 samples/samples-outofproc/TriggerBindingSamples/ProductsTriggerWithLeasesTableName.cs create mode 100644 samples/samples-powershell/ProductsTriggerLeasesTableName/function.json create mode 100644 samples/samples-powershell/ProductsTriggerLeasesTableName/run.ps1 create mode 100644 samples/samples-python/ProductsTriggerLeasesTableName/__init__.py create mode 100644 samples/samples-python/ProductsTriggerLeasesTableName/function.json diff --git a/samples/samples-csharp/local.settings.json b/samples/samples-csharp/local.settings.json index 6d3f9154b..153bc0dbf 100644 --- a/samples/samples-csharp/local.settings.json +++ b/samples/samples-csharp/local.settings.json @@ -6,6 +6,6 @@ "SqlConnectionString": "", "Sp_SelectCost": "SelectProductsCost", "ProductCost": 100, - "LeasesTableName": "leases" + "LeasesTableName": "Leases" } } \ No newline at end of file diff --git a/samples/samples-csx/ProductsTriggerLeasesTableName/function.json b/samples/samples-csx/ProductsTriggerLeasesTableName/function.json new file mode 100644 index 000000000..d7b45c1ff --- /dev/null +++ b/samples/samples-csx/ProductsTriggerLeasesTableName/function.json @@ -0,0 +1,13 @@ +{ + "bindings": [ + { + "name": "changes", + "type": "sqlTrigger", + "direction": "in", + "tableName": "dbo.Products", + "connectionStringSetting": "SqlConnectionString", + "leasesTableNameSetting": "LeasesTableName" + } + ], + "disabled": false +} \ No newline at end of file diff --git a/samples/samples-csx/ProductsTriggerLeasesTableName/run.csx b/samples/samples-csx/ProductsTriggerLeasesTableName/run.csx new file mode 100644 index 000000000..4a0cb01cf --- /dev/null +++ b/samples/samples-csx/ProductsTriggerLeasesTableName/run.csx @@ -0,0 +1,14 @@ +#load "../Common/product.csx" +#r "Newtonsoft.Json" +#r "Microsoft.Azure.WebJobs.Extensions.Sql" + +using System.Net; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Primitives; +using Newtonsoft.Json; +using Microsoft.Azure.WebJobs.Extensions.Sql; + +public static void Run(IReadOnlyList> changes, ILogger log) +{ + log.LogInformation("SQL Changes: " + JsonConvert.SerializeObject(changes)); +} \ No newline at end of file diff --git a/samples/samples-csx/local.settings.json b/samples/samples-csx/local.settings.json index 2ee8fdfec..153bc0dbf 100644 --- a/samples/samples-csx/local.settings.json +++ b/samples/samples-csx/local.settings.json @@ -5,6 +5,7 @@ "FUNCTIONS_WORKER_RUNTIME": "dotnet", "SqlConnectionString": "", "Sp_SelectCost": "SelectProductsCost", - "ProductCost": 100 + "ProductCost": 100, + "LeasesTableName": "Leases" } } \ No newline at end of file diff --git a/samples/samples-js/ProductsTriggerLeasesTableName/function.json b/samples/samples-js/ProductsTriggerLeasesTableName/function.json new file mode 100644 index 000000000..246eaa10a --- /dev/null +++ b/samples/samples-js/ProductsTriggerLeasesTableName/function.json @@ -0,0 +1,13 @@ +{ + "bindings": [ + { + "name": "changes", + "type": "sqlTrigger", + "direction": "in", + "tableName": "dbo.Products", + "connectionStringSetting": "SqlConnectionString", + "leasesTableNameSetting": "LeasesTableName" + } + ], + "disabled": false + } \ No newline at end of file diff --git a/samples/samples-js/ProductsTriggerLeasesTableName/index.js b/samples/samples-js/ProductsTriggerLeasesTableName/index.js new file mode 100644 index 000000000..5886e5c82 --- /dev/null +++ b/samples/samples-js/ProductsTriggerLeasesTableName/index.js @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +module.exports = async function (context, changes) { + context.log(`SQL Changes: ${JSON.stringify(changes)}`) +} \ No newline at end of file diff --git a/samples/samples-js/local.settings.json b/samples/samples-js/local.settings.json index d420cc65e..5ea6df3cc 100644 --- a/samples/samples-js/local.settings.json +++ b/samples/samples-js/local.settings.json @@ -5,6 +5,7 @@ "FUNCTIONS_WORKER_RUNTIME": "node", "SqlConnectionString": "", "Sp_SelectCost": "SelectProductsCost", - "ProductCost": 100 + "ProductCost": 100, + "LeasesTableName": "Leases" } } \ No newline at end of file diff --git a/samples/samples-outofproc/TriggerBindingSamples/ProductsTriggerWithLeasesTableName.cs b/samples/samples-outofproc/TriggerBindingSamples/ProductsTriggerWithLeasesTableName.cs new file mode 100644 index 000000000..3000e1195 --- /dev/null +++ b/samples/samples-outofproc/TriggerBindingSamples/ProductsTriggerWithLeasesTableName.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Extensions.Sql; +using Microsoft.Azure.WebJobs.Extensions.Sql.SamplesOutOfProc.Common; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using System; + +namespace Microsoft.Azure.WebJobs.Extensions.Sql.SamplesOutOfProc.TriggerBindingSamples +{ + public class ProductsTriggerWithLeasesTableName + { + private static readonly Action _loggerMessage = LoggerMessage.Define(LogLevel.Information, eventId: new EventId(0, "INFO"), formatString: "{Message}"); + + [Function("ProductsTriggerWithLeasesTableName")] + public static void Run( + [SqlTrigger("[dbo].[Products]", "SqlConnectionString", "LeasesTableName")] + IReadOnlyList> changes, FunctionContext context) + { + // The output is used to inspect the trigger binding parameter in test methods. + if (changes != null && changes.Count > 0) + { + _loggerMessage(context.GetLogger("ProductsTrigger"), "SQL Changes: " + JsonConvert.SerializeObject(changes), null); + } + } + } +} diff --git a/samples/samples-outofproc/local.settings.json b/samples/samples-outofproc/local.settings.json index fda4854dd..c11a5bb98 100644 --- a/samples/samples-outofproc/local.settings.json +++ b/samples/samples-outofproc/local.settings.json @@ -5,6 +5,7 @@ "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", "SqlConnectionString": "", "Sp_SelectCost": "SelectProductsCost", - "ProductCost": 100 + "ProductCost": 100, + "LeasesTableName": ":eases" } } \ No newline at end of file diff --git a/samples/samples-powershell/ProductsTriggerLeasesTableName/function.json b/samples/samples-powershell/ProductsTriggerLeasesTableName/function.json new file mode 100644 index 000000000..246eaa10a --- /dev/null +++ b/samples/samples-powershell/ProductsTriggerLeasesTableName/function.json @@ -0,0 +1,13 @@ +{ + "bindings": [ + { + "name": "changes", + "type": "sqlTrigger", + "direction": "in", + "tableName": "dbo.Products", + "connectionStringSetting": "SqlConnectionString", + "leasesTableNameSetting": "LeasesTableName" + } + ], + "disabled": false + } \ No newline at end of file diff --git a/samples/samples-powershell/ProductsTriggerLeasesTableName/run.ps1 b/samples/samples-powershell/ProductsTriggerLeasesTableName/run.ps1 new file mode 100644 index 000000000..6c13165f3 --- /dev/null +++ b/samples/samples-powershell/ProductsTriggerLeasesTableName/run.ps1 @@ -0,0 +1,10 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. + +using namespace System.Net + +param($changes) +# The output is used to inspect the trigger binding parameter in test methods. +# Use -Compress to remove new lines and spaces for testing purposes. +$changesJson = $changes | ConvertTo-Json -Compress +Write-Host "SQL Changes: $changesJson" \ No newline at end of file diff --git a/samples/samples-powershell/local.settings.json b/samples/samples-powershell/local.settings.json index aac210146..059188644 100644 --- a/samples/samples-powershell/local.settings.json +++ b/samples/samples-powershell/local.settings.json @@ -6,6 +6,7 @@ "FUNCTIONS_WORKER_RUNTIME_VERSION" : "~7.2", "SqlConnectionString": "", "Sp_SelectCost": "SelectProductsCost", - "ProductCost": 100 + "ProductCost": 100, + "LeasesTableName": "Leases" } } diff --git a/samples/samples-python/ProductsTriggerLeasesTableName/__init__.py b/samples/samples-python/ProductsTriggerLeasesTableName/__init__.py new file mode 100644 index 000000000..4ef0f7b40 --- /dev/null +++ b/samples/samples-python/ProductsTriggerLeasesTableName/__init__.py @@ -0,0 +1,8 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json +import logging + +def main(changes): + logging.info("SQL Changes: %s", json.loads(changes)) diff --git a/samples/samples-python/ProductsTriggerLeasesTableName/function.json b/samples/samples-python/ProductsTriggerLeasesTableName/function.json new file mode 100644 index 000000000..246eaa10a --- /dev/null +++ b/samples/samples-python/ProductsTriggerLeasesTableName/function.json @@ -0,0 +1,13 @@ +{ + "bindings": [ + { + "name": "changes", + "type": "sqlTrigger", + "direction": "in", + "tableName": "dbo.Products", + "connectionStringSetting": "SqlConnectionString", + "leasesTableNameSetting": "LeasesTableName" + } + ], + "disabled": false + } \ No newline at end of file diff --git a/samples/samples-python/local.settings.json b/samples/samples-python/local.settings.json index 687701584..295dfb8fb 100644 --- a/samples/samples-python/local.settings.json +++ b/samples/samples-python/local.settings.json @@ -5,6 +5,7 @@ "FUNCTIONS_WORKER_RUNTIME": "python", "SqlConnectionString": "", "Sp_SelectCost": "SelectProductsCost", - "ProductCost": 100 + "ProductCost": 100, + "LeasesTableName": "Leases" } } \ No newline at end of file From 51bdb0a4a53407a5fbe0d86feaaf21fa1bebafcb Mon Sep 17 00:00:00 2001 From: Lucy Zhang Date: Thu, 27 Jul 2023 07:40:53 -0700 Subject: [PATCH 04/11] remove setting --- .../src/SqlTriggerAttribute.cs | 10 ++++----- .../ProductsTriggerWithLeasesTableName.cs | 3 +-- samples/samples-csharp/local.settings.json | 3 +-- .../function.json | 2 +- samples/samples-csx/local.settings.json | 3 +-- .../function.json | 2 +- samples/samples-js/local.settings.json | 3 +-- .../ProductsTriggerWithLeasesTableName.cs | 3 +-- samples/samples-outofproc/local.settings.json | 3 +-- .../function.json | 2 +- .../samples-powershell/local.settings.json | 3 +-- .../function.json | 2 +- samples/samples-python/local.settings.json | 3 +-- src/SqlBindingUtilities.cs | 21 ------------------- src/TriggerBinding/SqlTriggerAttribute.cs | 10 ++++----- .../SqlTriggerBindingProvider.cs | 3 +-- src/TriggerBinding/SqlTriggerListener.cs | 7 ++++--- .../SqlTriggerBindingProviderTests.cs | 8 +++---- 18 files changed, 31 insertions(+), 60 deletions(-) diff --git a/Worker.Extensions.Sql/src/SqlTriggerAttribute.cs b/Worker.Extensions.Sql/src/SqlTriggerAttribute.cs index 698d74273..ed335c5ef 100644 --- a/Worker.Extensions.Sql/src/SqlTriggerAttribute.cs +++ b/Worker.Extensions.Sql/src/SqlTriggerAttribute.cs @@ -13,12 +13,12 @@ public sealed class SqlTriggerAttribute : TriggerBindingAttribute /// /// Name of the table to watch for changes. /// The name of the app setting where the SQL connection string is stored - /// The name of the app setting where the leases table name is stored - public SqlTriggerAttribute(string tableName, string connectionStringSetting, string leasesTableNameSetting = null) + /// Optional - The name of the table used to store leases + public SqlTriggerAttribute(string tableName, string connectionStringSetting, string leasesTableName = null) { this.TableName = tableName ?? throw new ArgumentNullException(nameof(tableName)); this.ConnectionStringSetting = connectionStringSetting ?? throw new ArgumentNullException(nameof(connectionStringSetting)); - this.LeasesTableNameSetting = leasesTableNameSetting; + this.LeasesTableName = leasesTableName; } /// @@ -32,11 +32,11 @@ public SqlTriggerAttribute(string tableName, string connectionStringSetting, str public string TableName { get; } /// - /// Name of the app setting containing the leases table name. + /// Name of the table used to store leases. /// If not specified, the leases table name will be Leases_{FunctionId}_{TableId} /// More information on how this is generated can be found here /// https://github.com/Azure/azure-functions-sql-extension/blob/release/trigger/docs/TriggerBinding.md#az_funcleases_ /// - public string LeasesTableNameSetting { get; } + public string LeasesTableName { get; } } } \ No newline at end of file diff --git a/samples/samples-csharp/TriggerBindingSamples/ProductsTriggerWithLeasesTableName.cs b/samples/samples-csharp/TriggerBindingSamples/ProductsTriggerWithLeasesTableName.cs index 5dfc7cf6a..34f2c0305 100644 --- a/samples/samples-csharp/TriggerBindingSamples/ProductsTriggerWithLeasesTableName.cs +++ b/samples/samples-csharp/TriggerBindingSamples/ProductsTriggerWithLeasesTableName.cs @@ -12,11 +12,10 @@ public static class ProductsTriggerWithLeasesTableName { [FunctionName(nameof(ProductsTriggerWithLeasesTableName))] public static void Run( - [SqlTrigger("[dbo].[Products]", "SqlConnectionString", "LeasesTableName")] + [SqlTrigger("[dbo].[Products]", "SqlConnectionString", "Leases")] IReadOnlyList> changes, ILogger logger) { - // The output is used to inspect the trigger binding parameter in test methods. logger.LogInformation("SQL Changes: " + JsonConvert.SerializeObject(changes)); } } diff --git a/samples/samples-csharp/local.settings.json b/samples/samples-csharp/local.settings.json index 153bc0dbf..2ee8fdfec 100644 --- a/samples/samples-csharp/local.settings.json +++ b/samples/samples-csharp/local.settings.json @@ -5,7 +5,6 @@ "FUNCTIONS_WORKER_RUNTIME": "dotnet", "SqlConnectionString": "", "Sp_SelectCost": "SelectProductsCost", - "ProductCost": 100, - "LeasesTableName": "Leases" + "ProductCost": 100 } } \ No newline at end of file diff --git a/samples/samples-csx/ProductsTriggerLeasesTableName/function.json b/samples/samples-csx/ProductsTriggerLeasesTableName/function.json index d7b45c1ff..8b9d204b1 100644 --- a/samples/samples-csx/ProductsTriggerLeasesTableName/function.json +++ b/samples/samples-csx/ProductsTriggerLeasesTableName/function.json @@ -6,7 +6,7 @@ "direction": "in", "tableName": "dbo.Products", "connectionStringSetting": "SqlConnectionString", - "leasesTableNameSetting": "LeasesTableName" + "leasesTableName": "Leases" } ], "disabled": false diff --git a/samples/samples-csx/local.settings.json b/samples/samples-csx/local.settings.json index 153bc0dbf..2ee8fdfec 100644 --- a/samples/samples-csx/local.settings.json +++ b/samples/samples-csx/local.settings.json @@ -5,7 +5,6 @@ "FUNCTIONS_WORKER_RUNTIME": "dotnet", "SqlConnectionString": "", "Sp_SelectCost": "SelectProductsCost", - "ProductCost": 100, - "LeasesTableName": "Leases" + "ProductCost": 100 } } \ No newline at end of file diff --git a/samples/samples-js/ProductsTriggerLeasesTableName/function.json b/samples/samples-js/ProductsTriggerLeasesTableName/function.json index 246eaa10a..075d638d9 100644 --- a/samples/samples-js/ProductsTriggerLeasesTableName/function.json +++ b/samples/samples-js/ProductsTriggerLeasesTableName/function.json @@ -6,7 +6,7 @@ "direction": "in", "tableName": "dbo.Products", "connectionStringSetting": "SqlConnectionString", - "leasesTableNameSetting": "LeasesTableName" + "leasesTableName": "Leases" } ], "disabled": false diff --git a/samples/samples-js/local.settings.json b/samples/samples-js/local.settings.json index 5ea6df3cc..d420cc65e 100644 --- a/samples/samples-js/local.settings.json +++ b/samples/samples-js/local.settings.json @@ -5,7 +5,6 @@ "FUNCTIONS_WORKER_RUNTIME": "node", "SqlConnectionString": "", "Sp_SelectCost": "SelectProductsCost", - "ProductCost": 100, - "LeasesTableName": "Leases" + "ProductCost": 100 } } \ No newline at end of file diff --git a/samples/samples-outofproc/TriggerBindingSamples/ProductsTriggerWithLeasesTableName.cs b/samples/samples-outofproc/TriggerBindingSamples/ProductsTriggerWithLeasesTableName.cs index 3000e1195..ba42a6aeb 100644 --- a/samples/samples-outofproc/TriggerBindingSamples/ProductsTriggerWithLeasesTableName.cs +++ b/samples/samples-outofproc/TriggerBindingSamples/ProductsTriggerWithLeasesTableName.cs @@ -17,10 +17,9 @@ public class ProductsTriggerWithLeasesTableName [Function("ProductsTriggerWithLeasesTableName")] public static void Run( - [SqlTrigger("[dbo].[Products]", "SqlConnectionString", "LeasesTableName")] + [SqlTrigger("[dbo].[Products]", "SqlConnectionString", "Leases")] IReadOnlyList> changes, FunctionContext context) { - // The output is used to inspect the trigger binding parameter in test methods. if (changes != null && changes.Count > 0) { _loggerMessage(context.GetLogger("ProductsTrigger"), "SQL Changes: " + JsonConvert.SerializeObject(changes), null); diff --git a/samples/samples-outofproc/local.settings.json b/samples/samples-outofproc/local.settings.json index c11a5bb98..fda4854dd 100644 --- a/samples/samples-outofproc/local.settings.json +++ b/samples/samples-outofproc/local.settings.json @@ -5,7 +5,6 @@ "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", "SqlConnectionString": "", "Sp_SelectCost": "SelectProductsCost", - "ProductCost": 100, - "LeasesTableName": ":eases" + "ProductCost": 100 } } \ No newline at end of file diff --git a/samples/samples-powershell/ProductsTriggerLeasesTableName/function.json b/samples/samples-powershell/ProductsTriggerLeasesTableName/function.json index 246eaa10a..075d638d9 100644 --- a/samples/samples-powershell/ProductsTriggerLeasesTableName/function.json +++ b/samples/samples-powershell/ProductsTriggerLeasesTableName/function.json @@ -6,7 +6,7 @@ "direction": "in", "tableName": "dbo.Products", "connectionStringSetting": "SqlConnectionString", - "leasesTableNameSetting": "LeasesTableName" + "leasesTableName": "Leases" } ], "disabled": false diff --git a/samples/samples-powershell/local.settings.json b/samples/samples-powershell/local.settings.json index 059188644..aac210146 100644 --- a/samples/samples-powershell/local.settings.json +++ b/samples/samples-powershell/local.settings.json @@ -6,7 +6,6 @@ "FUNCTIONS_WORKER_RUNTIME_VERSION" : "~7.2", "SqlConnectionString": "", "Sp_SelectCost": "SelectProductsCost", - "ProductCost": 100, - "LeasesTableName": "Leases" + "ProductCost": 100 } } diff --git a/samples/samples-python/ProductsTriggerLeasesTableName/function.json b/samples/samples-python/ProductsTriggerLeasesTableName/function.json index 246eaa10a..075d638d9 100644 --- a/samples/samples-python/ProductsTriggerLeasesTableName/function.json +++ b/samples/samples-python/ProductsTriggerLeasesTableName/function.json @@ -6,7 +6,7 @@ "direction": "in", "tableName": "dbo.Products", "connectionStringSetting": "SqlConnectionString", - "leasesTableNameSetting": "LeasesTableName" + "leasesTableName": "Leases" } ], "disabled": false diff --git a/samples/samples-python/local.settings.json b/samples/samples-python/local.settings.json index 295dfb8fb..687701584 100644 --- a/samples/samples-python/local.settings.json +++ b/samples/samples-python/local.settings.json @@ -5,7 +5,6 @@ "FUNCTIONS_WORKER_RUNTIME": "python", "SqlConnectionString": "", "Sp_SelectCost": "SelectProductsCost", - "ProductCost": 100, - "LeasesTableName": "Leases" + "ProductCost": 100 } } \ No newline at end of file diff --git a/src/SqlBindingUtilities.cs b/src/SqlBindingUtilities.cs index 72d1e7879..f2c304354 100644 --- a/src/SqlBindingUtilities.cs +++ b/src/SqlBindingUtilities.cs @@ -4,14 +4,12 @@ using System; using System.Collections.Generic; using System.Data; -using System.Globalization; using System.Linq; using System.Threading.Tasks; using System.Threading; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; -using static Microsoft.Azure.WebJobs.Extensions.Sql.SqlTriggerConstants; using static Microsoft.Azure.WebJobs.Extensions.Sql.Telemetry.Telemetry; using Microsoft.Azure.WebJobs.Extensions.Sql.Telemetry; @@ -56,25 +54,6 @@ public static string GetConnectionString(string connectionStringSetting, IConfig return connectionString; } - public static string GetLeasesTableName(string leasesTableNameSetting, IConfiguration configuration) - { - if (string.IsNullOrEmpty(leasesTableNameSetting)) - { - return ""; - } - if (configuration == null) - { - throw new ArgumentNullException(nameof(configuration)); - } - string leasesTableName = configuration.GetConnectionStringOrSetting(leasesTableNameSetting); - if (string.IsNullOrEmpty(leasesTableName)) - { - throw new ArgumentException(leasesTableName == null ? $"LeasesTableNameSetting '{leasesTableNameSetting}' is missing in your function app settings, please add the setting with a leases table name." : - $"LeasesTableNameSetting '{leasesTableNameSetting}' is empty in your function app settings, please update the setting with a leases table name."); - } - return string.Format(CultureInfo.InvariantCulture, UserDefinedLeasesTableNameFormat, $"{leasesTableName}"); - } - /// /// Parses the parameter string into a list of parameters, where each parameter is separated by "," and has the form /// "@param1=param2". "@param1" is the parameter name to be used in the query or stored procedure, and param1 is the diff --git a/src/TriggerBinding/SqlTriggerAttribute.cs b/src/TriggerBinding/SqlTriggerAttribute.cs index 6fa6f5dc8..8050708cd 100644 --- a/src/TriggerBinding/SqlTriggerAttribute.cs +++ b/src/TriggerBinding/SqlTriggerAttribute.cs @@ -18,12 +18,12 @@ public sealed class SqlTriggerAttribute : Attribute /// /// Name of the table to watch for changes. /// The name of the app setting where the SQL connection string is stored - /// The name of the app setting where the leases table name is stored - public SqlTriggerAttribute(string tableName, string connectionStringSetting, string leasesTableNameSetting = null) + /// Optional - The name of the table used to store leases + public SqlTriggerAttribute(string tableName, string connectionStringSetting, string leasesTableName = null) { this.TableName = tableName ?? throw new ArgumentNullException(nameof(tableName)); this.ConnectionStringSetting = connectionStringSetting ?? throw new ArgumentNullException(nameof(connectionStringSetting)); - this.LeasesTableNameSetting = leasesTableNameSetting; + this.LeasesTableName = leasesTableName; } /// @@ -38,11 +38,11 @@ public SqlTriggerAttribute(string tableName, string connectionStringSetting, str public string TableName { get; } /// - /// Name of the app setting containing the leases table name. + /// Name of the table used to store leases. /// If not specified, the leases table name will be Leases_{FunctionId}_{TableId} /// More information on how this is generated can be found here /// https://github.com/Azure/azure-functions-sql-extension/blob/release/trigger/docs/TriggerBinding.md#az_funcleases_ /// - public string LeasesTableNameSetting { get; } + public string LeasesTableName { get; } } } \ No newline at end of file diff --git a/src/TriggerBinding/SqlTriggerBindingProvider.cs b/src/TriggerBinding/SqlTriggerBindingProvider.cs index fa4f86315..26414775d 100644 --- a/src/TriggerBinding/SqlTriggerBindingProvider.cs +++ b/src/TriggerBinding/SqlTriggerBindingProvider.cs @@ -73,7 +73,6 @@ public Task TryCreateAsync(TriggerBindingProviderContext contex } string connectionString = SqlBindingUtilities.GetConnectionString(attribute.ConnectionStringSetting, this._configuration); - string leasesTableName = SqlBindingUtilities.GetLeasesTableName(attribute.LeasesTableNameSetting, this._configuration); Type bindingType; // Instantiate class 'SqlTriggerBinding' for non .NET In-Proc functions. @@ -91,7 +90,7 @@ public Task TryCreateAsync(TriggerBindingProviderContext contex var constructorParameterTypes = new Type[] { typeof(string), typeof(string), typeof(string), typeof(ParameterInfo), typeof(IHostIdProvider), typeof(ILogger), typeof(IConfiguration) }; ConstructorInfo bindingConstructor = bindingType.GetConstructor(constructorParameterTypes); - object[] constructorParameterValues = new object[] { connectionString, attribute.TableName, leasesTableName, parameter, this._hostIdProvider, this._logger, this._configuration }; + object[] constructorParameterValues = new object[] { connectionString, attribute.TableName, attribute.LeasesTableName, parameter, this._hostIdProvider, this._logger, this._configuration }; var triggerBinding = (ITriggerBinding)bindingConstructor.Invoke(constructorParameterValues); return Task.FromResult(triggerBinding); diff --git a/src/TriggerBinding/SqlTriggerListener.cs b/src/TriggerBinding/SqlTriggerListener.cs index dc5953ac1..cc2791c97 100644 --- a/src/TriggerBinding/SqlTriggerListener.cs +++ b/src/TriggerBinding/SqlTriggerListener.cs @@ -40,7 +40,7 @@ internal sealed class SqlTriggerListener : IListener, IScaleMonitorProvider, private readonly SqlObject _userTable; private readonly string _connectionString; - private readonly string _userDefinedLeasesTableName; + private readonly string _leasesTableName; private readonly string _userFunctionId; private readonly ITriggeredFunctionExecutor _executor; private readonly ILogger _logger; @@ -71,7 +71,7 @@ public SqlTriggerListener(string connectionString, string tableName, string leas { this._connectionString = !string.IsNullOrEmpty(connectionString) ? connectionString : throw new ArgumentNullException(nameof(connectionString)); this._userTable = !string.IsNullOrEmpty(tableName) ? new SqlObject(tableName) : throw new ArgumentNullException(nameof(tableName)); - this._userDefinedLeasesTableName = leasesTableName; + this._leasesTableName = leasesTableName; this._userFunctionId = !string.IsNullOrEmpty(userFunctionId) ? userFunctionId : throw new ArgumentNullException(nameof(userFunctionId)); this._executor = executor ?? throw new ArgumentNullException(nameof(executor)); this._logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -126,7 +126,8 @@ public async Task StartAsync(CancellationToken cancellationToken) IReadOnlyList<(string name, string type)> primaryKeyColumns = await GetPrimaryKeyColumnsAsync(connection, userTableId, this._logger, this._userTable.FullName, cancellationToken); IReadOnlyList userTableColumns = await this.GetUserTableColumnsAsync(connection, userTableId, cancellationToken); - string leasesTableName = this._userDefinedLeasesTableName ?? string.Format(CultureInfo.InvariantCulture, LeasesTableNameFormat, $"{this._userFunctionId}_{userTableId}"); + string leasesTableName = String.IsNullOrEmpty(this._leasesTableName) ? string.Format(CultureInfo.InvariantCulture, LeasesTableNameFormat, $"{this._userFunctionId}_{userTableId}") : + string.Format(CultureInfo.InvariantCulture, UserDefinedLeasesTableNameFormat, $"{this._leasesTableName}"); this._telemetryProps[TelemetryPropertyName.LeasesTableName] = leasesTableName; var transactionSw = Stopwatch.StartNew(); diff --git a/test/Unit/TriggerBinding/SqlTriggerBindingProviderTests.cs b/test/Unit/TriggerBinding/SqlTriggerBindingProviderTests.cs index 29c2d4926..97cd321f0 100644 --- a/test/Unit/TriggerBinding/SqlTriggerBindingProviderTests.cs +++ b/test/Unit/TriggerBinding/SqlTriggerBindingProviderTests.cs @@ -83,17 +83,17 @@ public async Task TryCreateAsync_ValidTriggerParameterType_ReturnsTriggerBinding /// required and optional properties set and it is applied on the trigger parameter of supported type. /// [Fact] - public async Task TryCreateAsync_TableName_ReturnsTriggerBinding() + public async Task TryCreateAsync_LeasesTableName_ReturnsTriggerBinding() { Type parameterType = typeof(IReadOnlyList>); - ITriggerBinding binding = await CreateTriggerBindingAsync(parameterType, nameof(UserFunctionWithTableName)); + ITriggerBinding binding = await CreateTriggerBindingAsync(parameterType, nameof(UserFunctionWithLeasesTableName)); Assert.IsType>(binding); } private static async Task CreateTriggerBindingAsync(Type parameterType, string methodName) { var provider = new SqlTriggerBindingProvider( - Mock.Of(c => c["testConnectionStringSetting"] == "testConnectionString" && c["testLeasesTableNameSetting"] == "testLeasesTableName"), + Mock.Of(c => c["testConnectionStringSetting"] == "testConnectionString"), Mock.Of(), Mock.Of(f => f.CreateLogger(It.IsAny()) == Mock.Of())); @@ -112,6 +112,6 @@ private static void UserFunctionWithoutConnectionString([SqlTrigger("testTabl private static void UserFunctionWithAttribute([SqlTrigger("testTableName", "testConnectionStringSetting")] T _) { } - private static void UserFunctionWithTableName([SqlTrigger("testTableName", "testConnectionStringSetting", "testLeasesTableNameSetting")] T _) { } + private static void UserFunctionWithLeasesTableName([SqlTrigger("testTableName", "testConnectionStringSetting", "testLeasesTableName")] T _) { } } } \ No newline at end of file From a7d0073ed6bfa0ce04386879b5e46f5d9dfa836a Mon Sep 17 00:00:00 2001 From: Lucy Zhang Date: Thu, 27 Jul 2023 07:41:13 -0700 Subject: [PATCH 05/11] add new constructor --- src/TriggerBinding/SqlTriggerAttribute.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/TriggerBinding/SqlTriggerAttribute.cs b/src/TriggerBinding/SqlTriggerAttribute.cs index 8050708cd..e5b6da78d 100644 --- a/src/TriggerBinding/SqlTriggerAttribute.cs +++ b/src/TriggerBinding/SqlTriggerAttribute.cs @@ -26,6 +26,14 @@ public SqlTriggerAttribute(string tableName, string connectionStringSetting, str this.LeasesTableName = leasesTableName; } + /// + /// Initializes a new instance of the class with default values for LeasesTableName. + /// + /// Name of the table to watch for changes. + /// The name of the app setting where the SQL connection string is stored + public SqlTriggerAttribute(string tableName, string connectionStringSetting) : this(tableName, connectionStringSetting, null) { } + + /// /// Name of the app setting containing the SQL connection string. /// From e7048c762fbb197d483dde7fff1e69e986147bcf Mon Sep 17 00:00:00 2001 From: Lucy Zhang Date: Thu, 27 Jul 2023 08:32:34 -0700 Subject: [PATCH 06/11] fix metrics provider --- src/TriggerBinding/SqlTriggerListener.cs | 16 ++++++++-------- src/TriggerBinding/SqlTriggerMetricsProvider.cs | 7 +++++-- src/TriggerBinding/SqlTriggerScaleMonitor.cs | 4 ++-- src/TriggerBinding/SqlTriggerTargetScaler.cs | 4 ++-- .../SqlTriggerBindingIntegrationTests.cs | 2 +- .../SqlTriggerScaleMonitorTests.cs | 2 ++ 6 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/TriggerBinding/SqlTriggerListener.cs b/src/TriggerBinding/SqlTriggerListener.cs index cc2791c97..034b4a4c1 100644 --- a/src/TriggerBinding/SqlTriggerListener.cs +++ b/src/TriggerBinding/SqlTriggerListener.cs @@ -40,7 +40,7 @@ internal sealed class SqlTriggerListener : IListener, IScaleMonitorProvider, private readonly SqlObject _userTable; private readonly string _connectionString; - private readonly string _leasesTableName; + private readonly string _userDefinedLeasesTableName; private readonly string _userFunctionId; private readonly ITriggeredFunctionExecutor _executor; private readonly ILogger _logger; @@ -62,16 +62,16 @@ internal sealed class SqlTriggerListener : IListener, IScaleMonitorProvider, /// /// SQL connection string used to connect to user database /// Name of the user table - /// Optional - Name of the leases table + /// Optional - Name of the leases table /// Unique identifier for the user function /// Defines contract for triggering user function /// Facilitates logging of messages /// Provides configuration values - public SqlTriggerListener(string connectionString, string tableName, string leasesTableName, string userFunctionId, ITriggeredFunctionExecutor executor, ILogger logger, IConfiguration configuration) + public SqlTriggerListener(string connectionString, string tableName, string userDefinedLeasesTableName, string userFunctionId, ITriggeredFunctionExecutor executor, ILogger logger, IConfiguration configuration) { this._connectionString = !string.IsNullOrEmpty(connectionString) ? connectionString : throw new ArgumentNullException(nameof(connectionString)); this._userTable = !string.IsNullOrEmpty(tableName) ? new SqlObject(tableName) : throw new ArgumentNullException(nameof(tableName)); - this._leasesTableName = leasesTableName; + this._userDefinedLeasesTableName = userDefinedLeasesTableName; this._userFunctionId = !string.IsNullOrEmpty(userFunctionId) ? userFunctionId : throw new ArgumentNullException(nameof(userFunctionId)); this._executor = executor ?? throw new ArgumentNullException(nameof(executor)); this._logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -85,8 +85,8 @@ public SqlTriggerListener(string connectionString, string tableName, string leas } this._hasConfiguredMaxChangesPerWorker = configuredMaxChangesPerWorker != null; - this._scaleMonitor = new SqlTriggerScaleMonitor(this._userFunctionId, this._userTable, this._connectionString, this._maxChangesPerWorker, this._logger); - this._targetScaler = new SqlTriggerTargetScaler(this._userFunctionId, this._userTable, this._connectionString, this._maxChangesPerWorker, this._logger); + this._scaleMonitor = new SqlTriggerScaleMonitor(this._userFunctionId, this._userTable, this._userDefinedLeasesTableName, this._connectionString, this._maxChangesPerWorker, this._logger); + this._targetScaler = new SqlTriggerTargetScaler(this._userFunctionId, this._userTable, this._userDefinedLeasesTableName, this._connectionString, this._maxChangesPerWorker, this._logger); } public void Cancel() @@ -126,8 +126,8 @@ public async Task StartAsync(CancellationToken cancellationToken) IReadOnlyList<(string name, string type)> primaryKeyColumns = await GetPrimaryKeyColumnsAsync(connection, userTableId, this._logger, this._userTable.FullName, cancellationToken); IReadOnlyList userTableColumns = await this.GetUserTableColumnsAsync(connection, userTableId, cancellationToken); - string leasesTableName = String.IsNullOrEmpty(this._leasesTableName) ? string.Format(CultureInfo.InvariantCulture, LeasesTableNameFormat, $"{this._userFunctionId}_{userTableId}") : - string.Format(CultureInfo.InvariantCulture, UserDefinedLeasesTableNameFormat, $"{this._leasesTableName}"); + string leasesTableName = String.IsNullOrEmpty(this._userDefinedLeasesTableName) ? string.Format(CultureInfo.InvariantCulture, LeasesTableNameFormat, $"{this._userFunctionId}_{userTableId}") : + string.Format(CultureInfo.InvariantCulture, UserDefinedLeasesTableNameFormat, $"{this._userDefinedLeasesTableName}"); this._telemetryProps[TelemetryPropertyName.LeasesTableName] = leasesTableName; var transactionSw = Stopwatch.StartNew(); diff --git a/src/TriggerBinding/SqlTriggerMetricsProvider.cs b/src/TriggerBinding/SqlTriggerMetricsProvider.cs index 67ad5ea6b..ca4ba6a00 100644 --- a/src/TriggerBinding/SqlTriggerMetricsProvider.cs +++ b/src/TriggerBinding/SqlTriggerMetricsProvider.cs @@ -27,13 +27,15 @@ internal class SqlTriggerMetricsProvider private readonly ILogger _logger; private readonly SqlObject _userTable; private readonly string _userFunctionId; + private readonly string _userDefinedLeasesTableName; - public SqlTriggerMetricsProvider(string connectionString, ILogger logger, SqlObject userTable, string userFunctionId) + public SqlTriggerMetricsProvider(string connectionString, ILogger logger, SqlObject userTable, string userFunctionId, string userDefinedLeasesTableName) { this._connectionString = !string.IsNullOrEmpty(connectionString) ? connectionString : throw new ArgumentNullException(nameof(connectionString)); this._logger = logger ?? throw new ArgumentNullException(nameof(logger)); this._userTable = userTable ?? throw new ArgumentNullException(nameof(userTable)); this._userFunctionId = !string.IsNullOrEmpty(userFunctionId) ? userFunctionId : throw new ArgumentNullException(nameof(userFunctionId)); + this._userDefinedLeasesTableName = userDefinedLeasesTableName; } public async Task GetMetricsAsync() { @@ -99,7 +101,8 @@ private async Task GetUnprocessedChangeCountAsync() private SqlCommand BuildGetUnprocessedChangesCommand(SqlConnection connection, SqlTransaction transaction, IReadOnlyList<(string name, string type)> primaryKeyColumns, int userTableId) { string leasesTableJoinCondition = string.Join(" AND ", primaryKeyColumns.Select(col => $"c.{col.name.AsBracketQuotedString()} = l.{col.name.AsBracketQuotedString()}")); - string leasesTableName = string.Format(CultureInfo.InvariantCulture, LeasesTableNameFormat, $"{this._userFunctionId}_{userTableId}"); + string leasesTableName = String.IsNullOrEmpty(this._userDefinedLeasesTableName) ? string.Format(CultureInfo.InvariantCulture, LeasesTableNameFormat, $"{this._userFunctionId}_{userTableId}") : + string.Format(CultureInfo.InvariantCulture, UserDefinedLeasesTableNameFormat, $"{this._userDefinedLeasesTableName}"); string getUnprocessedChangesQuery = $@" {AppLockStatements} diff --git a/src/TriggerBinding/SqlTriggerScaleMonitor.cs b/src/TriggerBinding/SqlTriggerScaleMonitor.cs index c30f54e30..d5f1fbf14 100644 --- a/src/TriggerBinding/SqlTriggerScaleMonitor.cs +++ b/src/TriggerBinding/SqlTriggerScaleMonitor.cs @@ -25,7 +25,7 @@ internal sealed class SqlTriggerScaleMonitor : IScaleMonitor private readonly IDictionary _telemetryProps = new Dictionary(); private readonly int _maxChangesPerWorker; - public SqlTriggerScaleMonitor(string userFunctionId, SqlObject userTable, string connectionString, int maxChangesPerWorker, ILogger logger) + public SqlTriggerScaleMonitor(string userFunctionId, SqlObject userTable, string userDefinedLeasesTableName, string connectionString, int maxChangesPerWorker, ILogger logger) { _ = !string.IsNullOrEmpty(userFunctionId) ? true : throw new ArgumentNullException(userFunctionId); _ = userTable != null ? true : throw new ArgumentNullException(nameof(userTable)); @@ -33,7 +33,7 @@ public SqlTriggerScaleMonitor(string userFunctionId, SqlObject userTable, string // Do not convert the scale-monitor ID to lower-case string since SQL table names can be case-sensitive // depending on the collation of the current database. this.Descriptor = new ScaleMonitorDescriptor($"{userFunctionId}-SqlTrigger-{this._userTable.FullName}", userFunctionId); - this._metricsProvider = new SqlTriggerMetricsProvider(connectionString, logger, this._userTable, userFunctionId); + this._metricsProvider = new SqlTriggerMetricsProvider(connectionString, logger, this._userTable, userFunctionId, userDefinedLeasesTableName); this._logger = logger ?? throw new ArgumentNullException(nameof(logger)); this._maxChangesPerWorker = maxChangesPerWorker; } diff --git a/src/TriggerBinding/SqlTriggerTargetScaler.cs b/src/TriggerBinding/SqlTriggerTargetScaler.cs index b1aabf5eb..7c8bda82a 100644 --- a/src/TriggerBinding/SqlTriggerTargetScaler.cs +++ b/src/TriggerBinding/SqlTriggerTargetScaler.cs @@ -15,9 +15,9 @@ internal sealed class SqlTriggerTargetScaler : ITargetScaler private readonly SqlTriggerMetricsProvider _metricsProvider; private readonly int _maxChangesPerWorker; - public SqlTriggerTargetScaler(string userFunctionId, SqlObject userTable, string connectionString, int maxChangesPerWorker, ILogger logger) + public SqlTriggerTargetScaler(string userFunctionId, SqlObject userTable, string userDefinedLeasesTableName, string connectionString, int maxChangesPerWorker, ILogger logger) { - this._metricsProvider = new SqlTriggerMetricsProvider(connectionString, logger, userTable, userFunctionId); + this._metricsProvider = new SqlTriggerMetricsProvider(connectionString, logger, userTable, userFunctionId, userDefinedLeasesTableName); this.TargetScalerDescriptor = new TargetScalerDescriptor(userFunctionId); this._maxChangesPerWorker = maxChangesPerWorker; } diff --git a/test/Integration/SqlTriggerBindingIntegrationTests.cs b/test/Integration/SqlTriggerBindingIntegrationTests.cs index 343891c9f..f0dc8067f 100644 --- a/test/Integration/SqlTriggerBindingIntegrationTests.cs +++ b/test/Integration/SqlTriggerBindingIntegrationTests.cs @@ -548,7 +548,7 @@ public async void GetMetricsTest() await listener.StartAsync(CancellationToken.None); // Cancel immediately so the listener doesn't start processing the changes await listener.StopAsync(CancellationToken.None); - var metricsProvider = new SqlTriggerMetricsProvider(this.DbConnectionString, Mock.Of(), new SqlObject("dbo.Products"), userFunctionId); + var metricsProvider = new SqlTriggerMetricsProvider(this.DbConnectionString, Mock.Of(), new SqlObject("dbo.Products"), userFunctionId, ""); SqlTriggerMetrics metrics = await metricsProvider.GetMetricsAsync(); Assert.True(metrics.UnprocessedChangeCount == 0, "There should initially be 0 unprocessed changes"); this.InsertProducts(1, 5); diff --git a/test/Unit/TriggerBinding/SqlTriggerScaleMonitorTests.cs b/test/Unit/TriggerBinding/SqlTriggerScaleMonitorTests.cs index 8bf093f7f..c2fb1793e 100644 --- a/test/Unit/TriggerBinding/SqlTriggerScaleMonitorTests.cs +++ b/test/Unit/TriggerBinding/SqlTriggerScaleMonitorTests.cs @@ -234,6 +234,7 @@ private static IScaleMonitor GetScaleMonitor(string tableName return new SqlTriggerScaleMonitor( userFunctionId, new SqlObject(tableName), + "testUserDefinedLeasesTableName", "testConnectionString", SqlTriggerListener.DefaultMaxChangesPerWorker, Mock.Of()); @@ -246,6 +247,7 @@ private static (IScaleMonitor monitor, List logMessag IScaleMonitor monitor = new SqlTriggerScaleMonitor( "testUserFunctionId", new SqlObject("testTableName"), + "testUserDefinedLeasesTableName", "testConnectionString", maxChangesPerWorker, mockLogger.Object); From 557e462a08cc5ea527047809cf7e8a0d1b4b2c32 Mon Sep 17 00:00:00 2001 From: Lucy Zhang Date: Thu, 27 Jul 2023 11:57:51 -0700 Subject: [PATCH 07/11] add integration test --- ...Name.cs => ProductsTriggerLeasesTableName.cs} | 4 ++-- ...Name.cs => ProductsTriggerLeasesTableName.cs} | 4 ++-- .../SqlTriggerBindingIntegrationTests.cs | 16 ++++++++++++++++ 3 files changed, 20 insertions(+), 4 deletions(-) rename samples/samples-csharp/TriggerBindingSamples/{ProductsTriggerWithLeasesTableName.cs => ProductsTriggerLeasesTableName.cs} (84%) rename samples/samples-outofproc/TriggerBindingSamples/{ProductsTriggerWithLeasesTableName.cs => ProductsTriggerLeasesTableName.cs} (91%) diff --git a/samples/samples-csharp/TriggerBindingSamples/ProductsTriggerWithLeasesTableName.cs b/samples/samples-csharp/TriggerBindingSamples/ProductsTriggerLeasesTableName.cs similarity index 84% rename from samples/samples-csharp/TriggerBindingSamples/ProductsTriggerWithLeasesTableName.cs rename to samples/samples-csharp/TriggerBindingSamples/ProductsTriggerLeasesTableName.cs index 34f2c0305..94f01458e 100644 --- a/samples/samples-csharp/TriggerBindingSamples/ProductsTriggerWithLeasesTableName.cs +++ b/samples/samples-csharp/TriggerBindingSamples/ProductsTriggerLeasesTableName.cs @@ -8,9 +8,9 @@ namespace Microsoft.Azure.WebJobs.Extensions.Sql.Samples.TriggerBindingSamples { - public static class ProductsTriggerWithLeasesTableName + public static class ProductsTriggerLeasesTableName { - [FunctionName(nameof(ProductsTriggerWithLeasesTableName))] + [FunctionName(nameof(ProductsTriggerLeasesTableName))] public static void Run( [SqlTrigger("[dbo].[Products]", "SqlConnectionString", "Leases")] IReadOnlyList> changes, diff --git a/samples/samples-outofproc/TriggerBindingSamples/ProductsTriggerWithLeasesTableName.cs b/samples/samples-outofproc/TriggerBindingSamples/ProductsTriggerLeasesTableName.cs similarity index 91% rename from samples/samples-outofproc/TriggerBindingSamples/ProductsTriggerWithLeasesTableName.cs rename to samples/samples-outofproc/TriggerBindingSamples/ProductsTriggerLeasesTableName.cs index ba42a6aeb..78f7b85f0 100644 --- a/samples/samples-outofproc/TriggerBindingSamples/ProductsTriggerWithLeasesTableName.cs +++ b/samples/samples-outofproc/TriggerBindingSamples/ProductsTriggerLeasesTableName.cs @@ -11,11 +11,11 @@ namespace Microsoft.Azure.WebJobs.Extensions.Sql.SamplesOutOfProc.TriggerBindingSamples { - public class ProductsTriggerWithLeasesTableName + public class ProductsTriggerLeasesTableName { private static readonly Action _loggerMessage = LoggerMessage.Define(LogLevel.Information, eventId: new EventId(0, "INFO"), formatString: "{Message}"); - [Function("ProductsTriggerWithLeasesTableName")] + [Function("ProductsTriggerLeasesTableName")] public static void Run( [SqlTrigger("[dbo].[Products]", "SqlConnectionString", "Leases")] IReadOnlyList> changes, FunctionContext context) diff --git a/test/Integration/SqlTriggerBindingIntegrationTests.cs b/test/Integration/SqlTriggerBindingIntegrationTests.cs index f0dc8067f..3ff9b7f06 100644 --- a/test/Integration/SqlTriggerBindingIntegrationTests.cs +++ b/test/Integration/SqlTriggerBindingIntegrationTests.cs @@ -805,5 +805,21 @@ void MonitorOutputData(object sender, DataReceivedEventArgs e) functionHost.OutputDataReceived -= MonitorOutputData; } } + + /// + /// Ensures that the user defined leasesTableName is used to create the leases table. + /// + [Theory] + [SqlInlineData()] + [UnsupportedLanguages(SupportedLanguages.Java)] + public void LeasesTableNameTest(SupportedLanguages lang) + { + this.SetChangeTrackingForTable("Products"); + int count = (int)this.ExecuteScalar("SELECT COUNT(*) FROM sys.tables WHERE [name] = 'Leases'"); + Assert.Equal(0, count); + this.StartFunctionHost(nameof(ProductsTriggerLeasesTableName), lang); + Thread.Sleep(5000); + Assert.Equal(1, (int)this.ExecuteScalar("SELECT COUNT(*) FROM sys.tables WHERE [name] = 'Leases'")); + } } } \ No newline at end of file From 454665c64dfd81c961fd72664d5bcef019dc0cd6 Mon Sep 17 00:00:00 2001 From: Lucy Zhang Date: Thu, 27 Jul 2023 12:19:52 -0700 Subject: [PATCH 08/11] fix oop --- Worker.Extensions.Sql/src/SqlTriggerAttribute.cs | 7 +++++++ src/TriggerBinding/SqlTriggerAttribute.cs | 1 - 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Worker.Extensions.Sql/src/SqlTriggerAttribute.cs b/Worker.Extensions.Sql/src/SqlTriggerAttribute.cs index ed335c5ef..e37474929 100644 --- a/Worker.Extensions.Sql/src/SqlTriggerAttribute.cs +++ b/Worker.Extensions.Sql/src/SqlTriggerAttribute.cs @@ -21,6 +21,13 @@ public SqlTriggerAttribute(string tableName, string connectionStringSetting, str this.LeasesTableName = leasesTableName; } + /// + /// Initializes a new instance of the class with default values for LeasesTableName. + /// + /// Name of the table to watch for changes. + /// The name of the app setting where the SQL connection string is stored + public SqlTriggerAttribute(string tableName, string connectionStringSetting) : this(tableName, connectionStringSetting, null) { } + /// /// Name of the app setting containing the SQL connection string. /// diff --git a/src/TriggerBinding/SqlTriggerAttribute.cs b/src/TriggerBinding/SqlTriggerAttribute.cs index e5b6da78d..f29b8865b 100644 --- a/src/TriggerBinding/SqlTriggerAttribute.cs +++ b/src/TriggerBinding/SqlTriggerAttribute.cs @@ -33,7 +33,6 @@ public SqlTriggerAttribute(string tableName, string connectionStringSetting, str /// The name of the app setting where the SQL connection string is stored public SqlTriggerAttribute(string tableName, string connectionStringSetting) : this(tableName, connectionStringSetting, null) { } - /// /// Name of the app setting containing the SQL connection string. /// From d6498b506f77c14827164812a87ede84eec40591 Mon Sep 17 00:00:00 2001 From: Lucy Zhang Date: Thu, 27 Jul 2023 12:58:09 -0700 Subject: [PATCH 09/11] fix test --- test/Integration/SqlTriggerBindingIntegrationTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/Integration/SqlTriggerBindingIntegrationTests.cs b/test/Integration/SqlTriggerBindingIntegrationTests.cs index 3ff9b7f06..56f1d6cab 100644 --- a/test/Integration/SqlTriggerBindingIntegrationTests.cs +++ b/test/Integration/SqlTriggerBindingIntegrationTests.cs @@ -814,6 +814,7 @@ void MonitorOutputData(object sender, DataReceivedEventArgs e) [UnsupportedLanguages(SupportedLanguages.Java)] public void LeasesTableNameTest(SupportedLanguages lang) { + this.ExecuteNonQuery("DROP TABLE IF EXISTS [az_func].[Leases]"); this.SetChangeTrackingForTable("Products"); int count = (int)this.ExecuteScalar("SELECT COUNT(*) FROM sys.tables WHERE [name] = 'Leases'"); Assert.Equal(0, count); From af00b1d6f2e71e980c7b24be3d387cf59befb960 Mon Sep 17 00:00:00 2001 From: Lucy Zhang Date: Thu, 27 Jul 2023 15:16:55 -0700 Subject: [PATCH 10/11] cleanup + pr comments --- Worker.Extensions.Sql/src/SqlTriggerAttribute.cs | 4 ++-- .../ProductsTriggerLeasesTableName.cs | 2 +- .../ProductsTriggerLeasesTableName/run.ps1 | 3 +-- src/TriggerBinding/SqlTriggerAttribute.cs | 4 ++-- src/TriggerBinding/SqlTriggerListener.cs | 4 +--- src/TriggerBinding/SqlTriggerMetricsProvider.cs | 4 +--- src/TriggerBinding/SqlTriggerUtils.cs | 14 ++++++++++++++ 7 files changed, 22 insertions(+), 13 deletions(-) diff --git a/Worker.Extensions.Sql/src/SqlTriggerAttribute.cs b/Worker.Extensions.Sql/src/SqlTriggerAttribute.cs index e37474929..42523923c 100644 --- a/Worker.Extensions.Sql/src/SqlTriggerAttribute.cs +++ b/Worker.Extensions.Sql/src/SqlTriggerAttribute.cs @@ -13,7 +13,7 @@ public sealed class SqlTriggerAttribute : TriggerBindingAttribute /// /// Name of the table to watch for changes. /// The name of the app setting where the SQL connection string is stored - /// Optional - The name of the table used to store leases + /// Optional - The name of the table used to store leases. If not specified, the leases table name will be Leases_{FunctionId}_{TableId} public SqlTriggerAttribute(string tableName, string connectionStringSetting, string leasesTableName = null) { this.TableName = tableName ?? throw new ArgumentNullException(nameof(tableName)); @@ -22,7 +22,7 @@ public SqlTriggerAttribute(string tableName, string connectionStringSetting, str } /// - /// Initializes a new instance of the class with default values for LeasesTableName. + /// Initializes a new instance of the class with null value for LeasesTableName. /// /// Name of the table to watch for changes. /// The name of the app setting where the SQL connection string is stored diff --git a/samples/samples-outofproc/TriggerBindingSamples/ProductsTriggerLeasesTableName.cs b/samples/samples-outofproc/TriggerBindingSamples/ProductsTriggerLeasesTableName.cs index 78f7b85f0..82943bd9b 100644 --- a/samples/samples-outofproc/TriggerBindingSamples/ProductsTriggerLeasesTableName.cs +++ b/samples/samples-outofproc/TriggerBindingSamples/ProductsTriggerLeasesTableName.cs @@ -22,7 +22,7 @@ public static void Run( { if (changes != null && changes.Count > 0) { - _loggerMessage(context.GetLogger("ProductsTrigger"), "SQL Changes: " + JsonConvert.SerializeObject(changes), null); + _loggerMessage(context.GetLogger("ProductsTriggerLeasesTableName"), "SQL Changes: " + JsonConvert.SerializeObject(changes), null); } } } diff --git a/samples/samples-powershell/ProductsTriggerLeasesTableName/run.ps1 b/samples/samples-powershell/ProductsTriggerLeasesTableName/run.ps1 index 6c13165f3..c990058f5 100644 --- a/samples/samples-powershell/ProductsTriggerLeasesTableName/run.ps1 +++ b/samples/samples-powershell/ProductsTriggerLeasesTableName/run.ps1 @@ -4,7 +4,6 @@ using namespace System.Net param($changes) -# The output is used to inspect the trigger binding parameter in test methods. -# Use -Compress to remove new lines and spaces for testing purposes. + $changesJson = $changes | ConvertTo-Json -Compress Write-Host "SQL Changes: $changesJson" \ No newline at end of file diff --git a/src/TriggerBinding/SqlTriggerAttribute.cs b/src/TriggerBinding/SqlTriggerAttribute.cs index f29b8865b..b1cc84497 100644 --- a/src/TriggerBinding/SqlTriggerAttribute.cs +++ b/src/TriggerBinding/SqlTriggerAttribute.cs @@ -18,7 +18,7 @@ public sealed class SqlTriggerAttribute : Attribute /// /// Name of the table to watch for changes. /// The name of the app setting where the SQL connection string is stored - /// Optional - The name of the table used to store leases + /// Optional - The name of the table used to store leases. If not specified, the leases table name will be Leases_{FunctionId}_{TableId} public SqlTriggerAttribute(string tableName, string connectionStringSetting, string leasesTableName = null) { this.TableName = tableName ?? throw new ArgumentNullException(nameof(tableName)); @@ -27,7 +27,7 @@ public SqlTriggerAttribute(string tableName, string connectionStringSetting, str } /// - /// Initializes a new instance of the class with default values for LeasesTableName. + /// Initializes a new instance of the class with null value for LeasesTableName. /// /// Name of the table to watch for changes. /// The name of the app setting where the SQL connection string is stored diff --git a/src/TriggerBinding/SqlTriggerListener.cs b/src/TriggerBinding/SqlTriggerListener.cs index 034b4a4c1..7d928a80b 100644 --- a/src/TriggerBinding/SqlTriggerListener.cs +++ b/src/TriggerBinding/SqlTriggerListener.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -126,8 +125,7 @@ public async Task StartAsync(CancellationToken cancellationToken) IReadOnlyList<(string name, string type)> primaryKeyColumns = await GetPrimaryKeyColumnsAsync(connection, userTableId, this._logger, this._userTable.FullName, cancellationToken); IReadOnlyList userTableColumns = await this.GetUserTableColumnsAsync(connection, userTableId, cancellationToken); - string leasesTableName = String.IsNullOrEmpty(this._userDefinedLeasesTableName) ? string.Format(CultureInfo.InvariantCulture, LeasesTableNameFormat, $"{this._userFunctionId}_{userTableId}") : - string.Format(CultureInfo.InvariantCulture, UserDefinedLeasesTableNameFormat, $"{this._userDefinedLeasesTableName}"); + string leasesTableName = GetLeasesTableName(this._userDefinedLeasesTableName, this._userFunctionId, userTableId); this._telemetryProps[TelemetryPropertyName.LeasesTableName] = leasesTableName; var transactionSw = Stopwatch.StartNew(); diff --git a/src/TriggerBinding/SqlTriggerMetricsProvider.cs b/src/TriggerBinding/SqlTriggerMetricsProvider.cs index ca4ba6a00..1a1c60c8c 100644 --- a/src/TriggerBinding/SqlTriggerMetricsProvider.cs +++ b/src/TriggerBinding/SqlTriggerMetricsProvider.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Data; using System.Diagnostics; -using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -101,8 +100,7 @@ private async Task GetUnprocessedChangeCountAsync() private SqlCommand BuildGetUnprocessedChangesCommand(SqlConnection connection, SqlTransaction transaction, IReadOnlyList<(string name, string type)> primaryKeyColumns, int userTableId) { string leasesTableJoinCondition = string.Join(" AND ", primaryKeyColumns.Select(col => $"c.{col.name.AsBracketQuotedString()} = l.{col.name.AsBracketQuotedString()}")); - string leasesTableName = String.IsNullOrEmpty(this._userDefinedLeasesTableName) ? string.Format(CultureInfo.InvariantCulture, LeasesTableNameFormat, $"{this._userFunctionId}_{userTableId}") : - string.Format(CultureInfo.InvariantCulture, UserDefinedLeasesTableNameFormat, $"{this._userDefinedLeasesTableName}"); + string leasesTableName = GetLeasesTableName(this._userDefinedLeasesTableName, this._userFunctionId, userTableId); string getUnprocessedChangesQuery = $@" {AppLockStatements} diff --git a/src/TriggerBinding/SqlTriggerUtils.cs b/src/TriggerBinding/SqlTriggerUtils.cs index 9d704be45..80b877248 100644 --- a/src/TriggerBinding/SqlTriggerUtils.cs +++ b/src/TriggerBinding/SqlTriggerUtils.cs @@ -3,11 +3,13 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; +using static Microsoft.Azure.WebJobs.Extensions.Sql.SqlTriggerConstants; namespace Microsoft.Azure.WebJobs.Extensions.Sql { @@ -111,5 +113,17 @@ internal static async Task GetUserTableIdAsync(SqlConnection connection, Sq return (int)userTableId; } } + + /// + /// Returns the formatted leases table name. If userDefinedLeasesTableName is null, the default name Leases_{FunctionId}_{TableId} is used. + /// + /// Leases table name defined by the user + /// SQL object ID of the user table + /// Unique identifier for the user function + internal static string GetLeasesTableName(string userDefinedLeasesTableName, string userFunctionId, int userTableId) + { + return string.IsNullOrEmpty(userDefinedLeasesTableName) ? string.Format(CultureInfo.InvariantCulture, LeasesTableNameFormat, $"{userFunctionId}_{userTableId}") : + string.Format(CultureInfo.InvariantCulture, UserDefinedLeasesTableNameFormat, $"{userDefinedLeasesTableName}"); + } } } \ No newline at end of file From a33cc13ed4b6a2f311ff9360063ec57c6a1b9725 Mon Sep 17 00:00:00 2001 From: Lucy Zhang Date: Thu, 27 Jul 2023 16:02:28 -0700 Subject: [PATCH 11/11] quote escape leasestablename --- src/TriggerBinding/SqlTriggerConstants.cs | 2 +- src/TriggerBinding/SqlTriggerUtils.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/TriggerBinding/SqlTriggerConstants.cs b/src/TriggerBinding/SqlTriggerConstants.cs index 6f360e101..4da60a0e0 100644 --- a/src/TriggerBinding/SqlTriggerConstants.cs +++ b/src/TriggerBinding/SqlTriggerConstants.cs @@ -11,7 +11,7 @@ internal static class SqlTriggerConstants public const string LeasesTableNameFormat = "[" + SchemaName + "].[Leases_{0}]"; - public const string UserDefinedLeasesTableNameFormat = "[" + SchemaName + "].[{0}]"; + public const string UserDefinedLeasesTableNameFormat = "[" + SchemaName + "].{0}"; public const string LeasesTableChangeVersionColumnName = "_az_func_ChangeVersion"; public const string LeasesTableAttemptCountColumnName = "_az_func_AttemptCount"; diff --git a/src/TriggerBinding/SqlTriggerUtils.cs b/src/TriggerBinding/SqlTriggerUtils.cs index 80b877248..43eb87f11 100644 --- a/src/TriggerBinding/SqlTriggerUtils.cs +++ b/src/TriggerBinding/SqlTriggerUtils.cs @@ -123,7 +123,7 @@ internal static async Task GetUserTableIdAsync(SqlConnection connection, Sq internal static string GetLeasesTableName(string userDefinedLeasesTableName, string userFunctionId, int userTableId) { return string.IsNullOrEmpty(userDefinedLeasesTableName) ? string.Format(CultureInfo.InvariantCulture, LeasesTableNameFormat, $"{userFunctionId}_{userTableId}") : - string.Format(CultureInfo.InvariantCulture, UserDefinedLeasesTableNameFormat, $"{userDefinedLeasesTableName}"); + string.Format(CultureInfo.InvariantCulture, UserDefinedLeasesTableNameFormat, $"{userDefinedLeasesTableName.AsBracketQuotedString()}"); } } } \ No newline at end of file