diff --git a/test-outofproc/ProductsColumnTypesTrigger.cs b/test-outofproc/ProductsColumnTypesTrigger.cs new file mode 100644 index 000000000..e1884a328 --- /dev/null +++ b/test-outofproc/ProductsColumnTypesTrigger.cs @@ -0,0 +1,27 @@ +// 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 DotnetIsolatedTests.Common; +using Microsoft.Extensions.Logging; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Extensions.Sql; + +namespace DotnetIsolatedTests +{ + public static class ProductsColumnTypesTrigger + { + /// + /// Simple trigger function used to verify different column types are serialized correctly. + /// + [Function(nameof(ProductsColumnTypesTrigger))] + public static void Run( + [SqlTrigger("[dbo].[ProductsColumnTypes]", "SqlConnectionString")] + IReadOnlyList> changes, + FunctionContext context) + { + ILogger logger = context.GetLogger("ProductsColumnTypesTrigger"); + logger.LogInformation("SQL Changes: " + Utils.JsonSerializeObject(changes)); + } + } +} \ No newline at end of file diff --git a/test/Integration/SqlTriggerBindingIntegrationTests.cs b/test/Integration/SqlTriggerBindingIntegrationTests.cs index d41fe2010..3fbb35204 100644 --- a/test/Integration/SqlTriggerBindingIntegrationTests.cs +++ b/test/Integration/SqlTriggerBindingIntegrationTests.cs @@ -654,7 +654,91 @@ JOIN sys.columns c //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."); + } + + /// + /// Ensures that all column types are serialized correctly. + /// + [Theory] + [SqlInlineData()] + public async Task ProductsColumnTypesTriggerTest(SupportedLanguages lang) + { + this.SetChangeTrackingForTable("ProductsColumnTypes"); + this.StartFunctionHost(nameof(ProductsColumnTypesTrigger), lang, true); + ProductColumnTypes expectedResponse = Utils.JsonDeserializeObject(/*lang=json,strict*/ "{\"ProductId\":999,\"BigInt\":999,\"Bit\":true,\"DecimalType\":1.2345,\"Money\":1.2345,\"Numeric\":1.2345,\"SmallInt\":1,\"SmallMoney\":1.2345,\"TinyInt\":1,\"FloatType\":0.1,\"Real\":0.1,\"Date\":\"2022-10-20T00:00:00.000Z\",\"Datetime\":\"2022-10-20T12:39:13.123Z\",\"Datetime2\":\"2022-10-20T12:39:13.123Z\",\"DatetimeOffset\":\"2022-10-20T12:39:13.123Z\",\"SmallDatetime\":\"2022-10-20T12:39:00.000Z\",\"Time\":\"12:39:13.1230000\",\"CharType\":\"test\",\"Varchar\":\"test\",\"Nchar\":\"test\",\"Nvarchar\":\"test\",\"Binary\":\"dGVzdA==\",\"Varbinary\":\"dGVzdA==\"}"); + int index = 0; + string messagePrefix = "SQL Changes: "; + + var taskCompletion = new TaskCompletionSource(); + void MonitorOutputData(object sender, DataReceivedEventArgs e) + { + if (e.Data != null && (index = e.Data.IndexOf(messagePrefix, StringComparison.Ordinal)) >= 0) + { + string json = e.Data[(index + messagePrefix.Length)..]; + // Sometimes we'll get messages that have extra logging content on the same line - so to prevent that from breaking + // the deserialization we look for the end of the changes array and only use that. + // (This is fine since we control what content is in the array so know that none of the items have a ] in them) + json = json[..(json.IndexOf(']') + 1)]; + IReadOnlyList> changes; + try + { + changes = Utils.JsonDeserializeObject>>(json); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Exception deserializing JSON content. Error={ex.Message} Json=\"{json}\"", ex); + } + Assert.Equal(SqlChangeOperation.Insert, changes[0].Operation); // Expected change operation + ProductColumnTypes product = changes[0].Item; + Assert.NotNull(product); // Product deserialized correctly + Assert.Equal(expectedResponse, product); // The product has the expected values + taskCompletion.SetResult(true); + } + }; + + // Set up listener for the changes coming in + foreach (Process functionHost in this.FunctionHostList) + { + functionHost.OutputDataReceived += MonitorOutputData; + } + + // Now that we've set up our listener trigger the actions to monitor + string datetime = "2022-10-20 12:39:13.123"; + this.ExecuteNonQuery("INSERT INTO [dbo].[ProductsColumnTypes] VALUES (" + + "999, " + // ProductId, + "999, " + // BigInt + "1, " + // Bit + "1.2345, " + // DecimalType + "1.2345, " + // Money + "1.2345, " + // Numeric + "1, " + // SmallInt + "1.2345, " + // SmallMoney + "1, " + // TinyInt + ".1, " + // FloatType + ".1, " + // Real + $"CONVERT(DATE, '{datetime}'), " + // Date + $"CONVERT(DATETIME, '{datetime}'), " + // Datetime + $"CONVERT(DATETIME2, '{datetime}'), " + // Datetime2 + $"CONVERT(DATETIMEOFFSET, '{datetime}'), " + // DatetimeOffset + $"CONVERT(SMALLDATETIME, '{datetime}'), " + // SmallDatetime + $"CONVERT(TIME, '{datetime}'), " + // Time + "'test', " + // CharType + "'test', " + // Varchar + "'test', " + // Nchar + "'test', " + // Nvarchar + "CONVERT(BINARY, 'test'), " + // Binary + "CONVERT(VARBINARY, 'test'))"); // Varbinary + + // Now wait until either we timeout or we've gotten all the expected changes, whichever comes first + this.LogOutput($"[{DateTime.UtcNow:u}] Waiting for Insert changes (10000ms)"); + await taskCompletion.Task.TimeoutAfter(TimeSpan.FromMilliseconds(10000), $"Timed out waiting for Insert changes."); + + // Unhook handler since we're done monitoring these changes so we aren't checking other changes done later + foreach (Process functionHost in this.FunctionHostList) + { + functionHost.OutputDataReceived -= MonitorOutputData; + } } } } \ No newline at end of file diff --git a/test/Integration/test-csharp/ProductsColumnTypesTrigger.cs b/test/Integration/test-csharp/ProductsColumnTypesTrigger.cs new file mode 100644 index 000000000..c3b6fa7e3 --- /dev/null +++ b/test/Integration/test-csharp/ProductsColumnTypesTrigger.cs @@ -0,0 +1,25 @@ +// 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.Tests.Common; +using Microsoft.Extensions.Logging; + + +namespace Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Integration +{ + public static class ProductsColumnTypesTrigger + { + /// + /// Simple trigger function used to verify different column types are serialized correctly. + /// + [FunctionName(nameof(ProductsColumnTypesTrigger))] + public static void Run( + [SqlTrigger("[dbo].[ProductsColumnTypes]", "SqlConnectionString")] + IReadOnlyList> changes, + ILogger logger) + { + logger.LogInformation("SQL Changes: " + Utils.JsonSerializeObject(changes)); + } + } +} \ No newline at end of file diff --git a/test/Integration/test-csx/ProductsColumnTypesTrigger/function.json b/test/Integration/test-csx/ProductsColumnTypesTrigger/function.json new file mode 100644 index 000000000..c1aca1832 --- /dev/null +++ b/test/Integration/test-csx/ProductsColumnTypesTrigger/function.json @@ -0,0 +1,12 @@ +{ + "bindings": [ + { + "name": "changes", + "type": "sqlTrigger", + "direction": "in", + "tableName": "dbo.ProductsColumnTypes", + "connectionStringSetting": "SqlConnectionString" + } + ], + "disabled": false +} \ No newline at end of file diff --git a/test/Integration/test-csx/ProductsColumnTypesTrigger/run.csx b/test/Integration/test-csx/ProductsColumnTypesTrigger/run.csx new file mode 100644 index 000000000..738cd4799 --- /dev/null +++ b/test/Integration/test-csx/ProductsColumnTypesTrigger/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: " + Microsoft.Azure.WebJobs.Extensions.Sql.Utils.JsonSerializeObject(changes)); +} \ No newline at end of file diff --git a/test/Integration/test-java/src/main/java/com/function/Common/SqlChangeProductColumnTypes.java b/test/Integration/test-java/src/main/java/com/function/Common/SqlChangeProductColumnTypes.java new file mode 100644 index 000000000..6bb222220 --- /dev/null +++ b/test/Integration/test-java/src/main/java/com/function/Common/SqlChangeProductColumnTypes.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.function.Common; + +public class SqlChangeProductColumnTypes { + private SqlChangeOperation Operation; + private ProductColumnTypes Item; + + public SqlChangeProductColumnTypes() { + } + + public SqlChangeProductColumnTypes(SqlChangeOperation operation, ProductColumnTypes item) { + this.Operation = operation; + this.Item = item; + } + + public SqlChangeOperation getOperation() { + return Operation; + } + + public void setOperation(SqlChangeOperation operation) { + this.Operation = operation; + } + + public ProductColumnTypes getItem() { + return Item; + } + + public void setItem(ProductColumnTypes item) { + this.Item = item; + } +} \ No newline at end of file diff --git a/test/Integration/test-java/src/main/java/com/function/ProductsColumnTypesTrigger.java b/test/Integration/test-java/src/main/java/com/function/ProductsColumnTypesTrigger.java new file mode 100644 index 000000000..c05f76740 --- /dev/null +++ b/test/Integration/test-java/src/main/java/com/function/ProductsColumnTypesTrigger.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.function; + +import com.function.Common.SqlChangeProductColumnTypes; +import com.google.gson.Gson; +import com.microsoft.azure.functions.ExecutionContext; +import com.microsoft.azure.functions.annotation.FunctionName; +import com.microsoft.azure.functions.sql.annotation.SQLTrigger; + +import java.util.logging.Level; + +public class ProductsColumnTypesTrigger { + @FunctionName("ProductsColumnTypesTrigger") + public void run( + @SQLTrigger( + name = "changes", + tableName = "[dbo].[ProductsColumnTypes]", + connectionStringSetting = "SqlConnectionString") + SqlChangeProductColumnTypes[] changes, + ExecutionContext context) throws Exception { + + context.getLogger().log(Level.INFO, "SQL Changes: " + new Gson().toJson(changes)); + } +} \ No newline at end of file diff --git a/test/Integration/test-js/ProductsColumnTypesTrigger/function.json b/test/Integration/test-js/ProductsColumnTypesTrigger/function.json new file mode 100644 index 000000000..3285e3baf --- /dev/null +++ b/test/Integration/test-js/ProductsColumnTypesTrigger/function.json @@ -0,0 +1,12 @@ +{ + "bindings": [ + { + "name": "changes", + "type": "sqlTrigger", + "direction": "in", + "tableName": "dbo.ProductsColumnTypes", + "connectionStringSetting": "SqlConnectionString" + } + ], + "disabled": false + } \ No newline at end of file diff --git a/test/Integration/test-js/ProductsColumnTypesTrigger/index.js b/test/Integration/test-js/ProductsColumnTypesTrigger/index.js new file mode 100644 index 000000000..5886e5c82 --- /dev/null +++ b/test/Integration/test-js/ProductsColumnTypesTrigger/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/test/Integration/test-powershell/ProductsColumnTypesTrigger/function.json b/test/Integration/test-powershell/ProductsColumnTypesTrigger/function.json new file mode 100644 index 000000000..3285e3baf --- /dev/null +++ b/test/Integration/test-powershell/ProductsColumnTypesTrigger/function.json @@ -0,0 +1,12 @@ +{ + "bindings": [ + { + "name": "changes", + "type": "sqlTrigger", + "direction": "in", + "tableName": "dbo.ProductsColumnTypes", + "connectionStringSetting": "SqlConnectionString" + } + ], + "disabled": false + } \ No newline at end of file diff --git a/test/Integration/test-powershell/ProductsColumnTypesTrigger/run.ps1 b/test/Integration/test-powershell/ProductsColumnTypesTrigger/run.ps1 new file mode 100644 index 000000000..e8c4559d6 --- /dev/null +++ b/test/Integration/test-powershell/ProductsColumnTypesTrigger/run.ps1 @@ -0,0 +1,9 @@ +# 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) + +$changesJson = $changes | ConvertTo-Json -Compress -AsArray +Write-Host "SQL Changes: $changesJson" \ No newline at end of file diff --git a/test/Integration/test-python/ProductsColumnTypesTrigger/__init__.py b/test/Integration/test-python/ProductsColumnTypesTrigger/__init__.py new file mode 100644 index 000000000..688c1e16c --- /dev/null +++ b/test/Integration/test-python/ProductsColumnTypesTrigger/__init__.py @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import logging + +def main(changes): + logging.info("SQL Changes: %s", changes) diff --git a/test/Integration/test-python/ProductsColumnTypesTrigger/function.json b/test/Integration/test-python/ProductsColumnTypesTrigger/function.json new file mode 100644 index 000000000..3285e3baf --- /dev/null +++ b/test/Integration/test-python/ProductsColumnTypesTrigger/function.json @@ -0,0 +1,12 @@ +{ + "bindings": [ + { + "name": "changes", + "type": "sqlTrigger", + "direction": "in", + "tableName": "dbo.ProductsColumnTypes", + "connectionStringSetting": "SqlConnectionString" + } + ], + "disabled": false + } \ No newline at end of file