Skip to content

Commit 032d811

Browse files
Throw a more helpful error message for unsupported types (#666)
* throw custom error for unsupported types * add ntext & image to test + cleanup * fix powershell ordering * Update src/SqlAsyncCollector.cs Co-authored-by: Charles Gagnon <chgagnon@microsoft.com> * update column names * fix tests * simplify --------- Co-authored-by: Charles Gagnon <chgagnon@microsoft.com>
1 parent 166e65c commit 032d811

File tree

18 files changed

+395
-3
lines changed

18 files changed

+395
-3
lines changed

src/SqlAsyncCollector.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ public enum QueryType
5757
/// <typeparam name="T">A user-defined POCO that represents a row of the user's table</typeparam>
5858
internal class SqlAsyncCollector<T> : IAsyncCollector<T>, IDisposable
5959
{
60+
private static readonly string[] UnsupportedTypes = { "NTEXT(*)", "TEXT(*)", "IMAGE(*)" };
6061
private const string RowDataParameter = "@rowData";
6162
private const string ColumnName = "COLUMN_NAME";
6263
private const string ColumnDefinition = "COLUMN_DEFINITION";
@@ -231,7 +232,15 @@ private async Task UpsertRowsAsync(IList<T> rows, SqlAttribute attribute, IConfi
231232
throw ex;
232233
}
233234

234-
IEnumerable<string> bracketedColumnNamesFromItem = GetColumnNamesFromItem(rows.First())
235+
IEnumerable<string> columnNamesFromItem = GetColumnNamesFromItem(rows.First());
236+
IEnumerable<string> unsupportedColumns = columnNamesFromItem.Where(prop => UnsupportedTypes.Contains(tableInfo.Columns[prop], StringComparer.OrdinalIgnoreCase));
237+
if (unsupportedColumns.Any())
238+
{
239+
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.";
240+
throw new InvalidOperationException(message);
241+
}
242+
243+
IEnumerable<string> bracketedColumnNamesFromItem = columnNamesFromItem
235244
.Where(prop => !tableInfo.PrimaryKeys.Any(k => k.IsIdentity && string.Equals(k.Name, prop, StringComparison.Ordinal))) // Skip any identity columns, those should never be updated
236245
.Select(prop => prop.AsBracketQuotedString());
237246
if (!bracketedColumnNamesFromItem.Any())
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using Microsoft.Azure.Functions.Worker;
5+
using Microsoft.Azure.Functions.Worker.Extensions.Sql;
6+
using DotnetIsolatedTests.Common;
7+
using Microsoft.AspNetCore.Http;
8+
9+
namespace DotnetIsolatedTests
10+
{
11+
public static class AddProductUnsupportedTypes
12+
{
13+
/// <summary>
14+
/// This output binding should fail since the target table has unsupported column types.
15+
/// </summary>
16+
[Function("AddProductUnsupportedTypes")]
17+
[SqlOutput("dbo.ProductsUnsupportedTypes", "SqlConnectionString")]
18+
public static ProductUnsupportedTypes Run(
19+
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "addproduct-unsupportedtypes")]
20+
HttpRequest req)
21+
{
22+
var product = new ProductUnsupportedTypes
23+
{
24+
ProductId = 1,
25+
TextCol = "test",
26+
NtextCol = "test",
27+
ImageCol = new byte[] { 1, 2, 3 }
28+
};
29+
return product;
30+
}
31+
}
32+
}

test-outofproc/GlobalSuppressions.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@
1414
[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")]
1515
[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}")]
1616
[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}")]
17-
[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")]
17+
[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")]
18+
[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")]

test-outofproc/Product.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,4 +188,15 @@ public class ProductMissingColumns
188188

189189
public string Name { get; set; }
190190
}
191+
192+
public class ProductUnsupportedTypes
193+
{
194+
public int ProductId { get; set; }
195+
196+
public string TextCol { get; set; }
197+
198+
public string NtextCol { get; set; }
199+
200+
public byte[] ImageCol { get; set; }
201+
}
191202
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
namespace Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Common
5+
{
6+
public class ProductUnsupportedTypes
7+
{
8+
public int ProductId { get; set; }
9+
10+
public string TextCol { get; set; }
11+
12+
public string NtextCol { get; set; }
13+
14+
public byte[] ImageCol { get; set; }
15+
}
16+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
CREATE TABLE [ProductsUnsupportedTypes] (
2+
[ProductId] [int] NOT NULL PRIMARY KEY,
3+
[TextCol] [text],
4+
[NtextCol] [ntext],
5+
[ImageCol] [image]
6+
)

test/GlobalSuppressions.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,5 @@
1919
[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})")]
2020
[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}")]
2121
[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")]
22-
[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")]
22+
[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")]
23+
[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")]

test/Integration/SqlOutputBindingIntegrationTests.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,5 +502,26 @@ public async Task NoPropertiesThrows(SupportedLanguages lang)
502502
// Wait 2sec for message to get processed to account for delays reading output
503503
await foundExpectedMessageSource.Task.TimeoutAfter(TimeSpan.FromMilliseconds(2000), $"Timed out waiting for expected error message");
504504
}
505+
506+
/// <summary>
507+
/// Tests that an error is thrown when the upserted item contains a unsupported column type.
508+
/// </summary>
509+
[Theory]
510+
[SqlInlineData()]
511+
[UnsupportedLanguages(SupportedLanguages.OutOfProc)]
512+
public async Task AddProductUnsupportedTypesTest(SupportedLanguages lang)
513+
{
514+
var foundExpectedMessageSource = new TaskCompletionSource<bool>();
515+
this.StartFunctionHost(nameof(AddProductUnsupportedTypes), lang, true, (object sender, DataReceivedEventArgs e) =>
516+
{
517+
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."))
518+
{
519+
foundExpectedMessageSource.SetResult(true);
520+
}
521+
});
522+
523+
Assert.Throws<AggregateException>(() => this.SendOutputGetRequest("addproduct-unsupportedtypes").Wait());
524+
await foundExpectedMessageSource.Task.TimeoutAfter(TimeSpan.FromMilliseconds(2000), $"Timed out waiting for expected error message");
525+
}
505526
}
506527
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using Microsoft.AspNetCore.Http;
5+
using Microsoft.AspNetCore.Mvc;
6+
using Microsoft.Azure.WebJobs.Extensions.Http;
7+
using Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Common;
8+
namespace Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Integration
9+
{
10+
public static class AddProductUnsupportedTypes
11+
{
12+
// This output binding should throw an exception because the target table has unsupported column types.
13+
[FunctionName("AddProductUnsupportedTypes")]
14+
public static IActionResult Run(
15+
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "addproduct-unsupportedtypes")]
16+
HttpRequest req,
17+
[Sql("dbo.ProductsUnsupportedTypes", "SqlConnectionString")] out ProductUnsupportedTypes product)
18+
{
19+
product = new ProductUnsupportedTypes()
20+
{
21+
ProductId = 1,
22+
TextCol = "test",
23+
NtextCol = "test",
24+
ImageCol = new byte[] { 1, 2, 3 }
25+
};
26+
return new CreatedResult($"/api/addproduct-unsupportedtypes", product);
27+
}
28+
}
29+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for
4+
* license information.
5+
*/
6+
7+
package com.function;
8+
9+
import com.microsoft.azure.functions.HttpMethod;
10+
import com.microsoft.azure.functions.HttpRequestMessage;
11+
import com.microsoft.azure.functions.HttpResponseMessage;
12+
import com.microsoft.azure.functions.HttpStatus;
13+
import com.microsoft.azure.functions.OutputBinding;
14+
import com.microsoft.azure.functions.annotation.AuthorizationLevel;
15+
import com.microsoft.azure.functions.annotation.FunctionName;
16+
import com.microsoft.azure.functions.annotation.HttpTrigger;
17+
import com.microsoft.azure.functions.sql.annotation.SQLOutput;
18+
import com.function.Common.ProductUnsupportedTypes;
19+
20+
import java.util.Optional;
21+
22+
23+
public class AddProductUnsupportedTypes {
24+
// This output binding should throw an exception because the target table has unsupported column types.
25+
@FunctionName("AddProductUnsupportedTypes")
26+
public HttpResponseMessage run(
27+
@HttpTrigger(
28+
name = "req",
29+
methods = {HttpMethod.GET},
30+
authLevel = AuthorizationLevel.ANONYMOUS,
31+
route = "addproduct-unsupportedtypes")
32+
HttpRequestMessage<Optional<String>> request,
33+
@SQLOutput(
34+
name = "product",
35+
commandText = "dbo.ProductsUnsupportedTypes",
36+
connectionStringSetting = "SqlConnectionString")
37+
OutputBinding<ProductUnsupportedTypes> product) {
38+
39+
ProductUnsupportedTypes p = new ProductUnsupportedTypes(
40+
0,
41+
"test",
42+
"test",
43+
"dGVzdA=="
44+
);
45+
product.setValue(p);
46+
return request.createResponseBuilder(HttpStatus.OK).header("Content-Type", "application/json").body(product).build();
47+
}
48+
}

0 commit comments

Comments
 (0)