Skip to content

Commit 2313e31

Browse files
Fix multiple item upsert with identity columns (#247)
* fix logic * Add test * Add justification * Add new function
1 parent fc3695d commit 2313e31

File tree

4 files changed

+92
-20
lines changed

4 files changed

+92
-20
lines changed

samples/samples-csharp/GlobalSuppressions.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,5 @@
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.OutputBindingSamples.TimerTriggerProducts.Run(Microsoft.Azure.WebJobs.TimerInfo,Microsoft.Extensions.Logging.ILogger,Microsoft.Azure.WebJobs.ICollector{Microsoft.Azure.WebJobs.Extensions.Sql.Samples.Common.Product})")]
2222
[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.GetProducts.Run(Microsoft.AspNetCore.Http.HttpRequest,System.Collections.Generic.IEnumerable{Microsoft.Azure.WebJobs.Extensions.Sql.Samples.Common.Product})~Microsoft.AspNetCore.Mvc.IActionResult")]
2323
[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.GetProductNamesView.Run(Microsoft.AspNetCore.Http.HttpRequest,System.Collections.Generic.IEnumerable{Microsoft.Azure.WebJobs.Extensions.Sql.Samples.Common.ProductName})~Microsoft.AspNetCore.Mvc.IActionResult")]
24-
[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.GetProductsStoredProcedureFromAppSetting.Run(Microsoft.AspNetCore.Http.HttpRequest,System.Collections.Generic.IEnumerable{Microsoft.Azure.WebJobs.Extensions.Sql.Samples.Common.Product})~Microsoft.AspNetCore.Mvc.IActionResult")]
24+
[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.GetProductsStoredProcedureFromAppSetting.Run(Microsoft.AspNetCore.Http.HttpRequest,System.Collections.Generic.IEnumerable{Microsoft.Azure.WebJobs.Extensions.Sql.Samples.Common.Product})~Microsoft.AspNetCore.Mvc.IActionResult")]
25+
[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.OutputBindingSamples.AddProductsWithIdentityColumnArray.Run(Microsoft.AspNetCore.Http.HttpRequest,Microsoft.Azure.WebJobs.Extensions.Sql.Samples.OutputBindingSamples.ProductWithoutId[]@)~Microsoft.AspNetCore.Mvc.IActionResult")]
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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+
8+
namespace Microsoft.Azure.WebJobs.Extensions.Sql.Samples.OutputBindingSamples
9+
{
10+
public static class AddProductsWithIdentityColumnArray
11+
{
12+
/// <summary>
13+
/// This shows an example of a SQL Output binding where the target table has a primary key
14+
/// which is an identity column. In such a case the primary key is not required to be in
15+
/// the object used by the binding - it will insert a row with the other values and the
16+
/// ID will be generated upon insert.
17+
/// </summary>
18+
/// <param name="req">The original request that triggered the function</param>
19+
/// <param name="product">The created Product object</param>
20+
/// <returns>The CreatedResult containing the new object that was inserted</returns>
21+
[FunctionName(nameof(AddProductsWithIdentityColumnArray))]
22+
public static IActionResult Run(
23+
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")]
24+
HttpRequest req,
25+
[Sql("dbo.ProductsWithIdentity", ConnectionStringSetting = "SqlConnectionString")] out ProductWithoutId[] products)
26+
{
27+
products = new[]
28+
{
29+
new ProductWithoutId
30+
{
31+
Name = "Cup",
32+
Cost = 2
33+
},
34+
new ProductWithoutId
35+
{
36+
Name = "Glasses",
37+
Cost = 12
38+
}
39+
};
40+
return new CreatedResult($"/api/addproductswithidentitycolumnarray", products);
41+
}
42+
}
43+
}

src/SqlAsyncCollector.cs

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -290,26 +290,36 @@ private static void GenerateDataQueryForMerge(TableInformation table, IEnumerabl
290290
{
291291
if (typeof(T) != typeof(JObject))
292292
{
293-
// SQL Server allows 900 bytes per primary key, so use that as a baseline
294-
var combinedPrimaryKey = new StringBuilder(900 * table.PrimaryKeys.Count());
295-
296-
// Look up primary key of T. Because we're going in the same order of fields every time,
297-
// we can assume that if two rows with the same primary key are in the list, they will collide
298-
foreach (PropertyInfo primaryKey in table.PrimaryKeys)
293+
if (table.HasIdentityColumnPrimaryKeys)
299294
{
300-
object value = primaryKey.GetValue(row);
301-
// Identity columns are allowed to be optional, so just skip the key if it doesn't exist
302-
if (value == null)
303-
{
304-
continue;
305-
}
306-
combinedPrimaryKey.Append(value.ToString());
295+
// If the table has an identity column as a primary key then
296+
// all rows are guaranteed to be unique so we can insert them all
297+
rowsToUpsert.Add(row);
307298
}
308-
// If we have already seen this unique primary key, skip this update
309-
if (uniqueUpdatedPrimaryKeys.Add(combinedPrimaryKey.ToString()))
299+
else
310300
{
311-
// This is the first time we've seen this particular PK. Add this row to the upsert query.
312-
rowsToUpsert.Add(row);
301+
// SQL Server allows 900 bytes per primary key, so use that as a baseline
302+
var combinedPrimaryKey = new StringBuilder(900 * table.PrimaryKeys.Count());
303+
// Look up primary key of T. Because we're going in the same order of fields every time,
304+
// we can assume that if two rows with the same primary key are in the list, they will collide
305+
foreach (PropertyInfo primaryKey in table.PrimaryKeys)
306+
{
307+
object value = primaryKey.GetValue(row);
308+
// Identity columns are allowed to be optional, so just skip the key if it doesn't exist
309+
if (value == null)
310+
{
311+
continue;
312+
}
313+
combinedPrimaryKey.Append(value.ToString());
314+
}
315+
string combinedPrimaryKeyStr = combinedPrimaryKey.ToString();
316+
// If we have already seen this unique primary key, skip this update
317+
// If the combined key is empty that means
318+
if (uniqueUpdatedPrimaryKeys.Add(combinedPrimaryKeyStr))
319+
{
320+
// This is the first time we've seen this particular PK. Add this row to the upsert query.
321+
rowsToUpsert.Add(row);
322+
}
313323
}
314324
}
315325
else
@@ -358,18 +368,23 @@ public class TableInformation
358368
/// </summary>
359369
public string Query { get; }
360370

371+
/// <summary>
372+
/// Whether at least one of the primary keys on this table is an identity column
373+
/// </summary>
374+
public bool HasIdentityColumnPrimaryKeys { get; }
361375
/// <summary>
362376
/// Settings to use when serializing the POCO into SQL.
363377
/// Only serialize properties and fields that correspond to SQL columns.
364378
/// </summary>
365379
public JsonSerializerSettings JsonSerializerSettings { get; }
366380

367-
public TableInformation(IEnumerable<MemberInfo> primaryKeys, IDictionary<string, string> columns, StringComparer comparer, string query)
381+
public TableInformation(IEnumerable<MemberInfo> primaryKeys, IDictionary<string, string> columns, StringComparer comparer, string query, bool hasIdentityColumnPrimaryKeys)
368382
{
369383
this.PrimaryKeys = primaryKeys;
370384
this.Columns = columns;
371385
this.Comparer = comparer;
372386
this.Query = query;
387+
this.HasIdentityColumnPrimaryKeys = hasIdentityColumnPrimaryKeys;
373388

374389
this.JsonSerializerSettings = new JsonSerializerSettings
375390
{
@@ -618,7 +633,7 @@ public static async Task<TableInformation> RetrieveTableInformationAsync(SqlConn
618633
sqlConnProps.Add(TelemetryPropertyName.QueryType.ToString(), usingInsertQuery ? "insert" : "merge");
619634
sqlConnProps.Add(TelemetryPropertyName.HasIdentityColumn.ToString(), hasIdentityColumnPrimaryKeys.ToString());
620635
TelemetryInstance.TrackDuration(TelemetryEventName.GetTableInfoEnd, tableInfoSw.ElapsedMilliseconds, sqlConnProps, durations);
621-
return new TableInformation(primaryKeyFields, columnDefinitionsFromSQL, comparer, query);
636+
return new TableInformation(primaryKeyFields, columnDefinitionsFromSQL, comparer, query, hasIdentityColumnPrimaryKeys);
622637
}
623638
}
624639

test/Integration/SqlOutputBindingIntegrationTests.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,19 @@ public void AddProductWithIdentity()
173173
Assert.Equal(1, this.ExecuteScalar("SELECT COUNT(*) FROM dbo.ProductsWithIdentity"));
174174
}
175175

176+
/// <summary>
177+
/// Tests that for tables with an identity column we are able to insert multiple items at once
178+
/// </summary>
179+
[Fact]
180+
public void AddProductsWithIdentityColumnArray()
181+
{
182+
this.StartFunctionHost(nameof(AddProductsWithIdentityColumnArray));
183+
Assert.Equal(0, this.ExecuteScalar("SELECT COUNT(*) FROM dbo.ProductsWithIdentity"));
184+
this.SendOutputRequest(nameof(AddProductsWithIdentityColumnArray)).Wait();
185+
// Multiple items should have been inserted
186+
Assert.Equal(2, this.ExecuteScalar("SELECT COUNT(*) FROM dbo.ProductsWithIdentity"));
187+
}
188+
176189
/// <summary>
177190
/// Tests that for tables with multiple primary columns (including an itemtity column) we are able to
178191
/// insert items.

0 commit comments

Comments
 (0)