Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add sql trigger test for different data types #876

Merged
merged 14 commits into from
Jun 28, 2023
27 changes: 27 additions & 0 deletions test-outofproc/ProductsColumnTypesTrigger.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Simple trigger function used to verify different column types are serialized correctly.
/// </summary>
[Function(nameof(ProductsColumnTypesTrigger))]
public static void Run(
[SqlTrigger("[dbo].[ProductsColumnTypes]", "SqlConnectionString")]
IReadOnlyList<SqlChange<ProductColumnTypes>> changes,
FunctionContext context)
{
ILogger logger = context.GetLogger("ProductsColumnTypesTrigger");
logger.LogInformation("SQL Changes: " + Utils.JsonSerializeObject(changes));
}
}
}
84 changes: 84 additions & 0 deletions test/Integration/SqlTriggerBindingIntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
}

/// <summary>
/// Ensures that all column types are serialized correctly.
/// </summary>
[Theory]
[SqlInlineData()]
public async Task ProductsColumnTypesTriggerTest(SupportedLanguages lang)
{
this.SetChangeTrackingForTable("ProductsColumnTypes");
this.StartFunctionHost(nameof(ProductsColumnTypesTrigger), lang, true);
ProductColumnTypes expectedResponse = Utils.JsonDeserializeObject<ProductColumnTypes>(/*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<bool>();

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<SqlChange<ProductColumnTypes>> changes;
try
{
changes = Utils.JsonDeserializeObject<IReadOnlyList<SqlChange<ProductColumnTypes>>>(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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I used NCHAR(0xD84C) here instead of 'test' (which is what we do in the input/output binding tests) the test failed in the pipeline but passed locally on my machine. Will try to keep investigating why it was failing in the pipelines but using 'test' for now so I can get the test in first.

"'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;
}
}
}
}
25 changes: 25 additions & 0 deletions test/Integration/test-csharp/ProductsColumnTypesTrigger.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Simple trigger function used to verify different column types are serialized correctly.
/// </summary>
[FunctionName(nameof(ProductsColumnTypesTrigger))]
public static void Run(
[SqlTrigger("[dbo].[ProductsColumnTypes]", "SqlConnectionString")]
IReadOnlyList<SqlChange<ProductColumnTypes>> changes,
ILogger logger)
{
logger.LogInformation("SQL Changes: " + Utils.JsonSerializeObject(changes));
}
}
}
12 changes: 12 additions & 0 deletions test/Integration/test-csx/ProductsColumnTypesTrigger/function.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"bindings": [
{
"name": "changes",
"type": "sqlTrigger",
"direction": "in",
"tableName": "dbo.ProductsColumnTypes",
"connectionStringSetting": "SqlConnectionString"
}
],
"disabled": false
}
14 changes: 14 additions & 0 deletions test/Integration/test-csx/ProductsColumnTypesTrigger/run.csx
Original file line number Diff line number Diff line change
@@ -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<SqlChange<ProductColumnTypes>> changes, ILogger log)
{
log.LogInformation("SQL Changes: " + Microsoft.Azure.WebJobs.Extensions.Sql.Utils.JsonSerializeObject(changes));
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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));
}
}
12 changes: 12 additions & 0 deletions test/Integration/test-js/ProductsColumnTypesTrigger/function.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"bindings": [
{
"name": "changes",
"type": "sqlTrigger",
"direction": "in",
"tableName": "dbo.ProductsColumnTypes",
"connectionStringSetting": "SqlConnectionString"
}
],
"disabled": false
}
6 changes: 6 additions & 0 deletions test/Integration/test-js/ProductsColumnTypesTrigger/index.js
Original file line number Diff line number Diff line change
@@ -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)}`)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"bindings": [
{
"name": "changes",
"type": "sqlTrigger",
"direction": "in",
"tableName": "dbo.ProductsColumnTypes",
"connectionStringSetting": "SqlConnectionString"
}
],
"disabled": false
}
Original file line number Diff line number Diff line change
@@ -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"
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"bindings": [
{
"name": "changes",
"type": "sqlTrigger",
"direction": "in",
"tableName": "dbo.ProductsColumnTypes",
"connectionStringSetting": "SqlConnectionString"
}
],
"disabled": false
}