diff --git a/src/TriggerBinding/SqlTriggerConstants.cs b/src/TriggerBinding/SqlTriggerConstants.cs index 276381b4a..16fbcdf3e 100644 --- a/src/TriggerBinding/SqlTriggerConstants.cs +++ b/src/TriggerBinding/SqlTriggerConstants.cs @@ -15,6 +15,7 @@ internal static class SqlTriggerConstants public const string LeasesTableAttemptCountColumnName = "_az_func_AttemptCount"; public const string LeasesTableLeaseExpirationTimeColumnName = "_az_func_LeaseExpirationTime"; public const string SysChangeVersionColumnName = "SYS_CHANGE_VERSION"; + public const string LastAccessTimeColumnName = "LastAccessTime"; /// /// The column names that are used in internal state tables and so can't exist in the target table /// since that shares column names with the primary keys from each user table being monitored. diff --git a/test/Integration/SqlTriggerBindingIntegrationTests.cs b/test/Integration/SqlTriggerBindingIntegrationTests.cs index 4d735b409..d41fe2010 100644 --- a/test/Integration/SqlTriggerBindingIntegrationTests.cs +++ b/test/Integration/SqlTriggerBindingIntegrationTests.cs @@ -9,6 +9,7 @@ using Microsoft.Azure.WebJobs.Extensions.Sql.Samples.Common; using Microsoft.Azure.WebJobs.Extensions.Sql.Samples.TriggerBindingSamples; using Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Common; +using static Microsoft.Azure.WebJobs.Extensions.Sql.SqlTriggerConstants; using Microsoft.Azure.WebJobs.Host.Executors; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; @@ -612,5 +613,48 @@ void TestExceptionMessageSeen(object sender, DataReceivedEventArgs e) await changesTask; } + + /// + /// Tests that the GlobalState table has LastAccessTime column. + /// + /// We call StartAsync which initializes the GlobalState and then drop the LastAccessTime column from the table and recall the StartAsync to check if the GlobalState has the column. + [Fact] + 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); + await listener.StartAsync(CancellationToken.None); + // Cancel immediately so the listener doesn't start processing the changes + await listener.StopAsync(CancellationToken.None); + //Check if LastAccessTime column exists in the GlobalState table + Assert.True(1 == (int)this.ExecuteScalar($@"SELECT 1 FROM sys.columns WHERE Name = N'{LastAccessTimeColumnName}' AND Object_ID = Object_ID(N'{GlobalStateTableName}')"), $"{GlobalStateTableName} should have {LastAccessTimeColumnName} column on creation"); + // Delete default constraint(s) on LastAccessTime column before dropping it + string deleteDefaultContraint = $@"DECLARE @sql NVARCHAR(MAX) + WHILE 1=1 + BEGIN + SELECT TOP 1 @sql = N'ALTER TABLE {GlobalStateTableName} DROP CONSTRAINT ['+dc.NAME+N']' + FROM sys.default_constraints dc + JOIN sys.columns c + ON c.default_object_id = dc.object_id + WHERE dc.parent_object_id = OBJECT_ID('{GlobalStateTableName}') + AND c.name = N'{LastAccessTimeColumnName}' + IF @@ROWCOUNT = 0 BREAK + EXEC (@sql) + END"; + this.ExecuteNonQuery(deleteDefaultContraint); + // Delete the LastAccessTime column from GlobalState table. + this.ExecuteNonQuery($"ALTER TABLE {GlobalStateTableName} DROP COLUMN {LastAccessTimeColumnName}"); + + await listener.StartAsync(CancellationToken.None); + // Cancel immediately so the listener doesn't start processing the changes + await listener.StopAsync(CancellationToken.None); + + //Check if LastAccessTime column exists in the GlobalState table + Assert.True(1 == (int)this.ExecuteScalar("SELECT 1 FROM sys.columns WHERE Name = N'LastAccessTime' AND Object_ID = Object_ID(N'[az_func].[GlobalState]')"), $"{GlobalStateTableName} should have {LastAccessTimeColumnName} column after restarting the listener."); + + } } } \ No newline at end of file