diff --git a/src/SqlAsyncCollector.cs b/src/SqlAsyncCollector.cs index f90c45e08..fb4f7161b 100644 --- a/src/SqlAsyncCollector.cs +++ b/src/SqlAsyncCollector.cs @@ -57,6 +57,7 @@ public enum QueryType /// A user-defined POCO that represents a row of the user's table internal class SqlAsyncCollector : IAsyncCollector, IDisposable { + private static readonly string[] UnsupportedTypes = { "NTEXT(*)", "TEXT(*)", "IMAGE(*)" }; private const string RowDataParameter = "@rowData"; private const string ColumnName = "COLUMN_NAME"; private const string ColumnDefinition = "COLUMN_DEFINITION"; @@ -231,7 +232,15 @@ private async Task UpsertRowsAsync(IList rows, SqlAttribute attribute, IConfi throw ex; } - IEnumerable bracketedColumnNamesFromItem = GetColumnNamesFromItem(rows.First()) + IEnumerable columnNamesFromItem = GetColumnNamesFromItem(rows.First()); + IEnumerable unsupportedColumns = columnNamesFromItem.Where(prop => UnsupportedTypes.Contains(tableInfo.Columns[prop], StringComparer.OrdinalIgnoreCase)); + if (unsupportedColumns.Any()) + { + string message = $"The type(s) of the following column(s) are not supported: {string.Join(", ", unsupportedColumns.ToArray())}. See https://github.com/Azure/azure-functions-sql-extension#output-bindings for more details."; + throw new InvalidOperationException(message); + } + + IEnumerable bracketedColumnNamesFromItem = columnNamesFromItem .Where(prop => !tableInfo.PrimaryKeys.Any(k => k.IsIdentity && string.Equals(k.Name, prop, StringComparison.Ordinal))) // Skip any identity columns, those should never be updated .Select(prop => prop.AsBracketQuotedString()); if (!bracketedColumnNamesFromItem.Any()) diff --git a/test-outofproc/AddProductUnsupportedTypes.cs b/test-outofproc/AddProductUnsupportedTypes.cs new file mode 100644 index 000000000..b6204f79f --- /dev/null +++ b/test-outofproc/AddProductUnsupportedTypes.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Extensions.Sql; +using DotnetIsolatedTests.Common; +using Microsoft.AspNetCore.Http; + +namespace DotnetIsolatedTests +{ + public static class AddProductUnsupportedTypes + { + /// + /// This output binding should fail since the target table has unsupported column types. + /// + [Function("AddProductUnsupportedTypes")] + [SqlOutput("dbo.ProductsUnsupportedTypes", "SqlConnectionString")] + public static ProductUnsupportedTypes Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "addproduct-unsupportedtypes")] + HttpRequest req) + { + var product = new ProductUnsupportedTypes + { + ProductId = 1, + TextCol = "test", + NtextCol = "test", + ImageCol = new byte[] { 1, 2, 3 } + }; + return product; + } + } +} diff --git a/test-outofproc/GlobalSuppressions.cs b/test-outofproc/GlobalSuppressions.cs index c83dffa97..98e12cbf1 100644 --- a/test-outofproc/GlobalSuppressions.cs +++ b/test-outofproc/GlobalSuppressions.cs @@ -14,4 +14,5 @@ [assembly: SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Unused parameter is required by functions binding", Scope = "member", Target = "~M:DotnetIsolatedTests.AddProductMissingColumnsExceptionFunction.Run(Microsoft.AspNetCore.Http.HttpRequest)~DotnetIsolatedTests.Common.ProductMissingColumns")] [assembly: SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Unused parameter is required by functions binding", Scope = "member", Target = "~M:DotnetIsolatedTests.AddProductsNoPartialUpsert.Run(Microsoft.AspNetCore.Http.HttpRequest)~System.Collections.Generic.List{DotnetIsolatedTests.Common.Product}")] [assembly: SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Unused parameter is required by functions binding", Scope = "member", Target = "~M:DotnetIsolatedTests.GetProductsColumnTypesSerialization.Run(Microsoft.AspNetCore.Http.HttpRequest,System.Collections.Generic.IEnumerable{DotnetIsolatedTests.Common.ProductColumnTypes})~System.Collections.Generic.IEnumerable{DotnetIsolatedTests.Common.ProductColumnTypes}")] -[assembly: SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Unused parameter is required by functions binding", Scope = "member", Target = "~M:DotnetIsolatedTests.AddProductIncorrectCasing.Run(Microsoft.Azure.Functions.Worker.Http.HttpRequestData)~DotnetIsolatedTests.Common.ProductIncorrectCasing")] \ No newline at end of file +[assembly: SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Unused parameter is required by functions binding", Scope = "member", Target = "~M:DotnetIsolatedTests.AddProductIncorrectCasing.Run(Microsoft.Azure.Functions.Worker.Http.HttpRequestData)~DotnetIsolatedTests.Common.ProductIncorrectCasing")] +[assembly: SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Unused parameter is required by functions binding", Scope = "member", Target = "~M:DotnetIsolatedTests.AddProductUnsupportedTypes.Run(Microsoft.AspNetCore.Http.HttpRequest)~DotnetIsolatedTests.Common.ProductUnsupportedTypes")] \ No newline at end of file diff --git a/test-outofproc/Product.cs b/test-outofproc/Product.cs index d797ff20e..789950ab3 100644 --- a/test-outofproc/Product.cs +++ b/test-outofproc/Product.cs @@ -188,4 +188,15 @@ public class ProductMissingColumns public string Name { get; set; } } + + public class ProductUnsupportedTypes + { + public int ProductId { get; set; } + + public string TextCol { get; set; } + + public string NtextCol { get; set; } + + public byte[] ImageCol { get; set; } + } } \ No newline at end of file diff --git a/test/Common/ProductUnsupportedTypes.cs b/test/Common/ProductUnsupportedTypes.cs new file mode 100644 index 000000000..c5fb39a5d --- /dev/null +++ b/test/Common/ProductUnsupportedTypes.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Common +{ + public class ProductUnsupportedTypes + { + public int ProductId { get; set; } + + public string TextCol { get; set; } + + public string NtextCol { get; set; } + + public byte[] ImageCol { get; set; } + } +} \ No newline at end of file diff --git a/test/Database/Tables/ProductsUnsupportedTypes.sql b/test/Database/Tables/ProductsUnsupportedTypes.sql new file mode 100644 index 000000000..8ce9b011c --- /dev/null +++ b/test/Database/Tables/ProductsUnsupportedTypes.sql @@ -0,0 +1,6 @@ +CREATE TABLE [ProductsUnsupportedTypes] ( + [ProductId] [int] NOT NULL PRIMARY KEY, + [TextCol] [text], + [NtextCol] [ntext], + [ImageCol] [image] +) \ No newline at end of file diff --git a/test/GlobalSuppressions.cs b/test/GlobalSuppressions.cs index 72e1de225..0664243d7 100644 --- a/test/GlobalSuppressions.cs +++ b/test/GlobalSuppressions.cs @@ -19,4 +19,5 @@ [assembly: SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Unused parameter is required by functions binding", Scope = "member", Target = "~M:Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Integration.UnsupportedColumnTypesTrigger.Run(System.Collections.Generic.IReadOnlyList{Microsoft.Azure.WebJobs.Extensions.Sql.SqlChange{Microsoft.Azure.WebJobs.Extensions.Sql.Samples.Common.Product})")] [assembly: SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Unused parameter is required by functions binding", Scope = "member", Target = "~M:Microsoft.Azure.WebJobs.Extensions.Sql.Samples.InputBindingSamples.GetProductsColumnTypesSerializationAsyncEnumerable.Run(Microsoft.AspNetCore.Http.HttpRequest,System.Collections.Generic.IAsyncEnumerable{Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Common.ProductColumnTypes},Microsoft.Extensions.Logging.ILogger)~System.Threading.Tasks.Task{Microsoft.AspNetCore.Mvc.IActionResult}")] [assembly: SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Unused parameter is required by functions binding", Scope = "member", Target = "~M:Microsoft.Azure.WebJobs.Extensions.Sql.Samples.InputBindingSamples.GetProductsColumnTypesSerialization.Run(Microsoft.AspNetCore.Http.HttpRequest,System.Collections.Generic.IEnumerable{Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Common.ProductColumnTypes},Microsoft.Extensions.Logging.ILogger)~Microsoft.AspNetCore.Mvc.IActionResult")] -[assembly: SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Unused parameter is required by functions binding", Scope = "member", Target = "~M:Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Integration.AddProductIncorrectCasing.Run(Microsoft.AspNetCore.Http.HttpRequest,Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Common.ProductIncorrectCasing@)~Microsoft.AspNetCore.Mvc.IActionResult")] \ No newline at end of file +[assembly: SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Unused parameter is required by functions binding", Scope = "member", Target = "~M:Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Integration.AddProductIncorrectCasing.Run(Microsoft.AspNetCore.Http.HttpRequest,Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Common.ProductIncorrectCasing@)~Microsoft.AspNetCore.Mvc.IActionResult")] +[assembly: SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Unused parameter is required by functions binding", Scope = "member", Target = "~M:Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Integration.AddProductUnsupportedTypes.Run(Microsoft.AspNetCore.Http.HttpRequest,Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Common.ProductUnsupportedTypes@)~Microsoft.AspNetCore.Mvc.IActionResult")] \ No newline at end of file diff --git a/test/Integration/SqlOutputBindingIntegrationTests.cs b/test/Integration/SqlOutputBindingIntegrationTests.cs index 87061700d..fd4e27c9a 100644 --- a/test/Integration/SqlOutputBindingIntegrationTests.cs +++ b/test/Integration/SqlOutputBindingIntegrationTests.cs @@ -501,5 +501,26 @@ public async Task NoPropertiesThrows(SupportedLanguages lang) // Wait 2sec for message to get processed to account for delays reading output await foundExpectedMessageSource.Task.TimeoutAfter(TimeSpan.FromMilliseconds(2000), $"Timed out waiting for expected error message"); } + + /// + /// Tests that an error is thrown when the upserted item contains a unsupported column type. + /// + [Theory] + [SqlInlineData()] + [UnsupportedLanguages(SupportedLanguages.OutOfProc)] + public async Task AddProductUnsupportedTypesTest(SupportedLanguages lang) + { + var foundExpectedMessageSource = new TaskCompletionSource(); + this.StartFunctionHost(nameof(AddProductUnsupportedTypes), lang, true, (object sender, DataReceivedEventArgs e) => + { + if (e.Data.Contains("The type(s) of the following column(s) are not supported: TextCol, NtextCol, ImageCol. See https://github.com/Azure/azure-functions-sql-extension#output-bindings for more details.")) + { + foundExpectedMessageSource.SetResult(true); + } + }); + + Assert.Throws(() => this.SendOutputGetRequest("addproduct-unsupportedtypes").Wait()); + await foundExpectedMessageSource.Task.TimeoutAfter(TimeSpan.FromMilliseconds(2000), $"Timed out waiting for expected error message"); + } } } diff --git a/test/Integration/test-csharp/AddProductUnsupportedTypes.cs b/test/Integration/test-csharp/AddProductUnsupportedTypes.cs new file mode 100644 index 000000000..6f4b4ccbe --- /dev/null +++ b/test/Integration/test-csharp/AddProductUnsupportedTypes.cs @@ -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. + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.WebJobs.Extensions.Http; +using Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Common; +namespace Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Integration +{ + public static class AddProductUnsupportedTypes + { + // This output binding should throw an exception because the target table has unsupported column types. + [FunctionName("AddProductUnsupportedTypes")] + public static IActionResult Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "addproduct-unsupportedtypes")] + HttpRequest req, + [Sql("dbo.ProductsUnsupportedTypes", "SqlConnectionString")] out ProductUnsupportedTypes product) + { + product = new ProductUnsupportedTypes() + { + ProductId = 1, + TextCol = "test", + NtextCol = "test", + ImageCol = new byte[] { 1, 2, 3 } + }; + return new CreatedResult($"/api/addproduct-unsupportedtypes", product); + } + } +} diff --git a/test/Integration/test-java/src/main/java/com/function/AddProductUnsupportedTypes.java b/test/Integration/test-java/src/main/java/com/function/AddProductUnsupportedTypes.java new file mode 100644 index 000000000..3be3e389d --- /dev/null +++ b/test/Integration/test-java/src/main/java/com/function/AddProductUnsupportedTypes.java @@ -0,0 +1,48 @@ +/** + * 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.microsoft.azure.functions.HttpMethod; +import com.microsoft.azure.functions.HttpRequestMessage; +import com.microsoft.azure.functions.HttpResponseMessage; +import com.microsoft.azure.functions.HttpStatus; +import com.microsoft.azure.functions.OutputBinding; +import com.microsoft.azure.functions.annotation.AuthorizationLevel; +import com.microsoft.azure.functions.annotation.FunctionName; +import com.microsoft.azure.functions.annotation.HttpTrigger; +import com.microsoft.azure.functions.sql.annotation.SQLOutput; +import com.function.Common.ProductUnsupportedTypes; + +import java.util.Optional; + + +public class AddProductUnsupportedTypes { + // This output binding should throw an exception because the target table has unsupported column types. + @FunctionName("AddProductUnsupportedTypes") + public HttpResponseMessage run( + @HttpTrigger( + name = "req", + methods = {HttpMethod.GET}, + authLevel = AuthorizationLevel.ANONYMOUS, + route = "addproduct-unsupportedtypes") + HttpRequestMessage> request, + @SQLOutput( + name = "product", + commandText = "dbo.ProductsUnsupportedTypes", + connectionStringSetting = "SqlConnectionString") + OutputBinding product) { + + ProductUnsupportedTypes p = new ProductUnsupportedTypes( + 0, + "test", + "test", + "dGVzdA==" + ); + product.setValue(p); + return request.createResponseBuilder(HttpStatus.OK).header("Content-Type", "application/json").body(product).build(); + } +} diff --git a/test/Integration/test-java/src/main/java/com/function/Common/ProductUnsupportedTypes.java b/test/Integration/test-java/src/main/java/com/function/Common/ProductUnsupportedTypes.java new file mode 100644 index 000000000..163c82336 --- /dev/null +++ b/test/Integration/test-java/src/main/java/com/function/Common/ProductUnsupportedTypes.java @@ -0,0 +1,62 @@ +/** + * 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; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class ProductUnsupportedTypes { + @JsonProperty("ProductId") + private int ProductId; + @JsonProperty("TextCol") + private String TextCol; + @JsonProperty("NtextCol") + private String NtextCol; + @JsonProperty("ImageCol") + private String ImageCol; + + public ProductUnsupportedTypes() { + } + + public ProductUnsupportedTypes(int productId, String textCol, String ntextCol, String imageCol) { + ProductId = productId; + TextCol = textCol; + NtextCol = ntextCol; + ImageCol = imageCol; + } + + public int getProductId() { + return ProductId; + } + + public void setProductId(int productId) { + ProductId = productId; + } + + public String getTextCol() { + return TextCol; + } + + public void setTextCol(String textCol) { + TextCol = textCol; + } + + public String getNtextCol() { + return NtextCol; + } + + public void setNtextCol(String ntextCol) { + NtextCol = ntextCol; + } + + public String getImageCol() { + return ImageCol; + } + + public void setImageCol(String imageCol) { + ImageCol = imageCol; + } +} \ No newline at end of file diff --git a/test/Integration/test-js/AddProductUnsupportedTypes/function.json b/test/Integration/test-js/AddProductUnsupportedTypes/function.json new file mode 100644 index 000000000..e9f3de84a --- /dev/null +++ b/test/Integration/test-js/AddProductUnsupportedTypes/function.json @@ -0,0 +1,27 @@ +{ + "bindings": [ + { + "authLevel": "function", + "name": "req", + "direction": "in", + "type": "httpTrigger", + "methods": [ + "get" + ], + "route": "addproduct-unsupportedtypes" + }, + { + "name": "$return", + "type": "http", + "direction": "out" + }, + { + "name": "product", + "type": "sql", + "direction": "out", + "commandText": "[dbo].[ProductsUnsupportedTypes]", + "connectionStringSetting": "SqlConnectionString" + } + ], + "disabled": false +} \ No newline at end of file diff --git a/test/Integration/test-js/AddProductUnsupportedTypes/index.js b/test/Integration/test-js/AddProductUnsupportedTypes/index.js new file mode 100644 index 000000000..27fd65388 --- /dev/null +++ b/test/Integration/test-js/AddProductUnsupportedTypes/index.js @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +// This output binding should throw an exception because the target table has unsupported column types. +module.exports = async function (context, req) { + context.bindings.product = { + ProductId: 0, + TextCol: "test", + NtextCol: "test", + ImageCol: "dGVzdA==" + } + + return { + status: 201, + body: req.body + }; +} \ No newline at end of file diff --git a/test/Integration/test-powershell/AddProductUnsupportedTypes/function.json b/test/Integration/test-powershell/AddProductUnsupportedTypes/function.json new file mode 100644 index 000000000..741b973ad --- /dev/null +++ b/test/Integration/test-powershell/AddProductUnsupportedTypes/function.json @@ -0,0 +1,27 @@ +{ + "bindings": [ + { + "authLevel": "function", + "name": "Request", + "direction": "in", + "type": "httpTrigger", + "methods": [ + "get" + ], + "route": "addproduct-unsupportedtypes" + }, + { + "name": "response", + "type": "http", + "direction": "out" + }, + { + "name": "product", + "type": "sql", + "direction": "out", + "commandText": "[dbo].[ProductsUnsupportedTypes]", + "connectionStringSetting": "SqlConnectionString" + } + ], + "disabled": false +} \ No newline at end of file diff --git a/test/Integration/test-powershell/AddProductUnsupportedTypes/run.ps1 b/test/Integration/test-powershell/AddProductUnsupportedTypes/run.ps1 new file mode 100644 index 000000000..9794efb58 --- /dev/null +++ b/test/Integration/test-powershell/AddProductUnsupportedTypes/run.ps1 @@ -0,0 +1,21 @@ +# 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 + +# This output binding should throw an exception because the target table has unsupported column types. +param($Request) + +$req_body = [ordered]@{ + ProductId=0; + TextCol="test"; + NtextCol="test"; + ImageCol="dGVzdA=="; +} + +Push-OutputBinding -Name product -Value $req_body + +Push-OutputBinding -Name response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = $req_body +}) \ No newline at end of file diff --git a/test/Integration/test-python/AddProductUnsupportedTypes/__init__.py b/test/Integration/test-python/AddProductUnsupportedTypes/__init__.py new file mode 100644 index 000000000..3f3a382eb --- /dev/null +++ b/test/Integration/test-python/AddProductUnsupportedTypes/__init__.py @@ -0,0 +1,24 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json +import azure.functions as func +from Common.productunsupportedtypes import ProductUnsupportedTypes + +def main(req: func.HttpRequest, product: func.Out[func.SqlRow]) -> func.HttpResponse: + """This output binding should throw an exception because the target table has unsupported column types. + """ + + row = func.SqlRow(ProductUnsupportedTypes( + 0, + "test", + "test", + "dGVzdA==" + )) + product.set(row) + + return func.HttpResponse( + body=req.get_body(), + status_code=201, + mimetype="application/json" + ) diff --git a/test/Integration/test-python/AddProductUnsupportedTypes/function.json b/test/Integration/test-python/AddProductUnsupportedTypes/function.json new file mode 100644 index 000000000..1a3280294 --- /dev/null +++ b/test/Integration/test-python/AddProductUnsupportedTypes/function.json @@ -0,0 +1,28 @@ +{ + "scriptFile": "__init__.py", + "bindings": [ + { + "authLevel": "function", + "name": "req", + "direction": "in", + "type": "httpTrigger", + "methods": [ + "get" + ], + "route": "addproduct-unsupportedtypes" + }, + { + "name": "$return", + "type": "http", + "direction": "out" + }, + { + "name": "product", + "type": "sql", + "direction": "out", + "commandText": "[dbo].[ProductsUnsupportedTypes]", + "connectionStringSetting": "SqlConnectionString" + } + ], + "disabled": false +} diff --git a/test/Integration/test-python/Common/productunsupportedtypes.py b/test/Integration/test-python/Common/productunsupportedtypes.py new file mode 100644 index 000000000..5002c68c8 --- /dev/null +++ b/test/Integration/test-python/Common/productunsupportedtypes.py @@ -0,0 +1,12 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import collections + +class ProductUnsupportedTypes(collections.UserDict): + def __init__(self, productId, textCol, ntextCol, imageCol): + super().__init__() + self['ProductId'] = productId + self['TextCol'] = textCol + self['NtextCol'] = ntextCol + self['ImageCol'] = imageCol \ No newline at end of file