Skip to content

Commit 9af5f20

Browse files
authored
Fix output binding when using column names with slashes (#1042)
* replace the / and \ slash while building the query * filter the test for .net inproc tests * update test for back slash * update test * fix
1 parent d3c48a4 commit 9af5f20

File tree

11 files changed

+198
-3
lines changed

11 files changed

+198
-3
lines changed

builds/azure-pipelines/template-steps-build-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ steps:
182182
projects: '${{ parameters.solution }}'
183183
# Skip any non .NET In-Proc integration tests. Otherwise, the following error will be thrown:
184184
# System.InvalidOperationException : No data found for Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Integration.SqlOutputBindingIntegrationTests.NoPropertiesThrows
185-
arguments: --configuration ${{ parameters.configuration }} --filter FullyQualifiedName!~NoPropertiesThrows --collect "Code Coverage" -s $(Build.SourcesDirectory)/test/coverage.runsettings --no-build
185+
arguments: --configuration ${{ parameters.configuration }} --filter "FullyQualifiedName!~NoPropertiesThrows & FullyQualifiedName!~AddProductWithSlashInColumnName" --collect "Code Coverage" -s $(Build.SourcesDirectory)/test/coverage.runsettings --no-build
186186
condition: and(succeededOrFailed(), eq(variables['Agent.OS'], 'Windows_NT'))
187187

188188
- task: DotNetCoreCLI@2

src/SqlAsyncCollector.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,8 @@ private static void GenerateDataQueryForMerge(TableInformation table, IEnumerabl
412412
rowData = Utils.JsonSerializeObject(rowsToUpsert, table.JsonSerializerSettings);
413413
IEnumerable<string> columnNamesFromItem = GetColumnNamesFromItem(rows.First());
414414
IEnumerable<string> bracketColumnDefinitionsFromItem = columnNamesFromItem.Select(c => $"{c.AsBracketQuotedString()} {table.Columns[c]}");
415-
newDataQuery = $"WITH {CteName} AS ( SELECT * FROM OPENJSON({RowDataParameter}) WITH ({string.Join(",", bracketColumnDefinitionsFromItem)}) )";
415+
// Escape any forward and backward slashes in the column names of rowData using REPLACE so the OPENJSON can read from those columns.
416+
newDataQuery = $"WITH {CteName} AS ( SELECT * FROM OPENJSON(REPLACE({RowDataParameter}, N'/', N'\\/')) WITH ({string.Join(",", bracketColumnDefinitionsFromItem)}) )";
416417
}
417418

418419
public class TableInformation
@@ -633,7 +634,7 @@ public static TableInformation RetrieveTableInformation(SqlConnection sqlConnect
633634
throw new InvalidOperationException(message, ex);
634635
}
635636

636-
if (!primaryKeys.Any())
637+
if (primaryKeys.Count == 0)
637638
{
638639
string message = $"Did not retrieve any primary keys for {table}. Cannot generate upsert command without them.";
639640
var ex = new InvalidOperationException(message);
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
DROP TABLE IF EXISTS [ProductsWithSlashInColumnNames];
2+
3+
CREATE TABLE [ProductsWithSlashInColumnNames] (
4+
[ProductId] [int] NOT NULL PRIMARY KEY,
5+
[Name/Test] [nchar](100) NOT NULL,
6+
[Cost\Test] [int] NOT NULL
7+
)

test/Integration/SqlOutputBindingIntegrationTests.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,5 +551,34 @@ public async Task AddProductDefaultPKAndDifferentColumnOrderTest(SupportedLangua
551551
await this.SendOutputGetRequest("addproductdefaultpkanddifferentcolumnorder", null, TestUtils.GetPort(lang, true));
552552
Assert.Equal(1, this.ExecuteScalar("SELECT COUNT(*) FROM dbo.ProductsWithDefaultPK"));
553553
}
554+
555+
/// <summary>
556+
/// Tests that when using a table with column names having slash (/ or \) in them
557+
/// we can insert data using output binding.
558+
/// Excluding C#, JAVA and CSX languages since / or \ are reserved characters and are not allowed to use in variable names
559+
/// </summary>
560+
[Theory]
561+
[SqlInlineData()]
562+
[UnsupportedLanguages(SupportedLanguages.CSharp, SupportedLanguages.OutOfProc, SupportedLanguages.Java, SupportedLanguages.Csx)]
563+
public async Task AddProductWithSlashInColumnName(SupportedLanguages lang)
564+
{
565+
Assert.Equal(0, this.ExecuteScalar("SELECT COUNT(*) FROM dbo.ProductsWithSlashInColumnNames"));
566+
this.StartFunctionHost("AddProductWithSlashInColumnName", lang);
567+
await this.SendOutputPostRequest("addproduct-slashcolumns", "");
568+
// Check that a product should have been inserted
569+
Assert.Equal("Test", this.ExecuteScalar("SELECT [Name/Test] FROM dbo.ProductsWithSlashInColumnNames WHERE ProductId = 1"));
570+
Assert.Equal(1, this.ExecuteScalar("SELECT [Cost\\Test] FROM dbo.ProductsWithSlashInColumnNames WHERE ProductId = 1"));
571+
572+
var query = new Dictionary<string, object>()
573+
{
574+
{ "ProductId", 2},
575+
{ "Name/Test", "Test" },
576+
{ "Cost\\Test", 2 }
577+
};
578+
await this.SendOutputPostRequest("addproduct-slashcolumns", Utils.JsonSerializeObject(query));
579+
// Check that a product should have been inserted
580+
Assert.Equal("Test", this.ExecuteScalar("SELECT [Name/Test] FROM dbo.ProductsWithSlashInColumnNames WHERE ProductId = 2"));
581+
Assert.Equal(2, this.ExecuteScalar("SELECT [Cost\\Test] FROM dbo.ProductsWithSlashInColumnNames WHERE ProductId = 2"));
582+
}
554583
}
555584
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"bindings": [
3+
{
4+
"authLevel": "function",
5+
"name": "req",
6+
"direction": "in",
7+
"type": "httpTrigger",
8+
"methods": [
9+
"post"
10+
],
11+
"route": "addproduct-slashcolumns"
12+
},
13+
{
14+
"name": "$return",
15+
"type": "http",
16+
"direction": "out"
17+
},
18+
{
19+
"name": "products",
20+
"type": "sql",
21+
"direction": "out",
22+
"commandText": "[dbo].[ProductsWithSlashInColumnNames]",
23+
"connectionStringSetting": "SqlConnectionString"
24+
}
25+
],
26+
"disabled": false
27+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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+
// This output binding should successfully add the productMissingColumns object
5+
// to the SQL table.
6+
module.exports = async function (context, req) {
7+
const productWithSlashInColumnName = req.body ?? [{
8+
"ProductId": 1,
9+
"Name/Test": "Test",
10+
"Cost\\Test": 1
11+
}];
12+
13+
context.bindings.products = JSON.stringify(productWithSlashInColumnName);
14+
15+
return {
16+
status: 201,
17+
body: productWithSlashInColumnName
18+
};
19+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"bindings": [
3+
{
4+
"authLevel": "function",
5+
"name": "Request",
6+
"direction": "in",
7+
"type": "httpTrigger",
8+
"methods": [
9+
"post"
10+
],
11+
"route": "addproduct-slashcolumns"
12+
},
13+
{
14+
"name": "response",
15+
"type": "http",
16+
"direction": "out"
17+
},
18+
{
19+
"name": "product",
20+
"type": "sql",
21+
"direction": "out",
22+
"commandText": "[dbo].[ProductsWithSlashInColumnNames]",
23+
"connectionStringSetting": "SqlConnectionString"
24+
}
25+
],
26+
"disabled": false
27+
}
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 namespace System.Net
5+
6+
# Trigger binding data passed in via param block
7+
param($Request, $TriggerMetadata)
8+
9+
# Write to the Azure Functions log stream.
10+
Write-Host "PowerShell function with SQL Output Binding processed a request."
11+
12+
# Note that this expects the body to be a JSON object or array of objects
13+
# which have a property matching each of the columns in the table to upsert to.
14+
$req_body = $Request.Body ? $Request.Body : @{
15+
"ProductId"="1";
16+
"Name/Test"="Test";
17+
"Cost\Test"="1"
18+
};
19+
20+
# Assign the value we want to pass to the SQL Output binding.
21+
# The -Name value corresponds to the name property in the function.json for the binding
22+
Push-OutputBinding -Name product -Value $req_body
23+
24+
# Assign the value to return as the HTTP response.
25+
# The -Name value matches the name property in the function.json for the binding
26+
Push-OutputBinding -Name response -Value ([HttpResponseContext]@{
27+
StatusCode = [HttpStatusCode]::OK
28+
Body = $req_body
29+
})
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
import json
5+
import azure.functions as func
6+
from Common.productWithSlash import ProductWithSlash
7+
8+
# This output binding should successfully add the productMissingColumns object
9+
# to the SQL table.
10+
def main(req: func.HttpRequest, product: func.Out[func.SqlRow]) -> func.HttpResponse:
11+
productWithSlash = func.SqlRow.from_dict(json.loads(req.get_body())) if req.get_body() else func.SqlRow(ProductWithSlash(1, "Test", 1))
12+
product.set(productWithSlash)
13+
14+
return func.HttpResponse(
15+
body=productWithSlash.to_json(),
16+
status_code=201,
17+
mimetype="application/json"
18+
)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"bindings": [
3+
{
4+
"authLevel": "function",
5+
"name": "req",
6+
"direction": "in",
7+
"type": "httpTrigger",
8+
"methods": [
9+
"post"
10+
],
11+
"route": "addproduct-slashcolumns"
12+
},
13+
{
14+
"name": "$return",
15+
"type": "http",
16+
"direction": "out"
17+
},
18+
{
19+
"name": "product",
20+
"type": "sql",
21+
"direction": "out",
22+
"commandText": "[dbo].[ProductsWithSlashInColumnNames]",
23+
"connectionStringSetting": "SqlConnectionString"
24+
}
25+
],
26+
"disabled": false
27+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
import collections
5+
6+
class ProductWithSlash(collections.UserDict):
7+
def __init__(self, productId, name, cost):
8+
super().__init__()
9+
self['ProductId'] = productId
10+
self['Name/Test'] = name
11+
self['Cost\Test'] = cost

0 commit comments

Comments
 (0)