diff --git a/Activities/Database/UiPath.Database.Activities/ExecuteNonQuery.cs b/Activities/Database/UiPath.Database.Activities/ExecuteNonQuery.cs index 5c07f952..ed828b93 100644 --- a/Activities/Database/UiPath.Database.Activities/ExecuteNonQuery.cs +++ b/Activities/Database/UiPath.Database.Activities/ExecuteNonQuery.cs @@ -47,7 +47,7 @@ protected async override Task> ExecuteAsync(Asy string sql = string.Empty; int? commandTimeoutMs = TimeoutMS.Expression is null ? (int?)null : TimeoutMS.Get(context); DatabaseConnection existingConnection = null; - DBExecuteCommandResult affectedRecords = null; + DBExecuteCommandResult affectedRecords = new DBExecuteCommandResult(); if (commandTimeoutMs.HasValue && commandTimeoutMs.Value < 0) { throw new ArgumentException(Resources.TimeoutMSException, nameof(TimeoutMS)); @@ -66,7 +66,7 @@ protected async override Task> ExecuteAsync(Asy parameters = ConnectionHelper.BuildParameters(Parameters, context); ConnectionHelper.ConnectionValidation(existingConnection, connSecureString, connString, provName); // create the action for doing the actual work - affectedRecords = await Task.Run(() => ExecuteCommand(connString, connSecureString, provName, sql, parameters, commandTimeout)); + affectedRecords = await Task.Run(() => ExecuteCommand(connString, connSecureString, provName, sql, parameters, commandTimeout), cancellationToken); } catch (Exception ex) { @@ -110,7 +110,7 @@ private DBExecuteCommandResult ExecuteCommand(string connString, SecureString co return new DBExecuteCommandResult(DbConnection.Execute(sql, parameters, commandTimeout, CommandType), parameters); } - private class DBExecuteCommandResult + private sealed class DBExecuteCommandResult { public int Result { get; } public Dictionary ParametersBind { get; } diff --git a/Activities/Database/UiPath.Database.Tests/SqliteIntegrationTests.cs b/Activities/Database/UiPath.Database.Tests/SqliteIntegrationTests.cs index 7132a25c..86036bef 100644 --- a/Activities/Database/UiPath.Database.Tests/SqliteIntegrationTests.cs +++ b/Activities/Database/UiPath.Database.Tests/SqliteIntegrationTests.cs @@ -257,7 +257,7 @@ public void DatabaseTransaction_InternalConnection_OutputBound_IsNotDisposed() try { var connVar = new Variable(); - var capture = new CaptureConnection { Input = new InArgument(connVar) }; + var capture = new CaptureValue { Input = new InArgument(connVar) }; var workflow = new Sequence { @@ -320,6 +320,67 @@ public void DatabaseTransaction_InternalConnection_OutputNotBound_IsDisposed() } } + [Fact, TestPriority(12)] + public void ExecuteNonQuery_ContinueOnError_SwallowsExceptionInsteadOfThrowingNullReferenceException() + { + var activity = new ExecuteNonQuery + { + ExistingDbConnection = new InArgument(_ => _fixture.Connection), + Sql = new InArgument("SELECT * FROM ThisTableDoesNotExist_Probe"), + ContinueOnError = new InArgument(true), + AffectedRecords = new OutArgument() + }; + + var outputs = WorkflowInvoker.Invoke(activity, TimeSpan.FromSeconds(30)); + + var affected = (int)outputs[nameof(ExecuteNonQuery.AffectedRecords)]; + Assert.Equal(0, affected); + } + + [Fact, TestPriority(13)] + public void ExecuteNonQuery_ContinueOnError_ResetsAffectedRecordsInsteadOfLeavingStaleValue() + { + // Pre-seed the bound variable with a value the activity did NOT produce, so the + // assertion can only pass if ExecuteNonQuery actively resets it to 0 on the swallowed + // failure path, not because an unbound OutArgument happens to default to 0. + var affectedVar = new Variable { Default = 42 }; + var capture = new CaptureValue { Input = new InArgument(affectedVar) }; + + var workflow = new Sequence + { + Variables = { affectedVar }, + Activities = + { + new ExecuteNonQuery + { + ExistingDbConnection = new InArgument(_ => _fixture.Connection), + Sql = new InArgument("SELECT * FROM ThisTableDoesNotExist_Probe"), + ContinueOnError = new InArgument(true), + AffectedRecords = new OutArgument(affectedVar) + }, + capture + } + }; + + WorkflowInvoker.Invoke(workflow, TimeSpan.FromSeconds(30)); + + Assert.Equal(0, capture.Value); + } + + [Fact, TestPriority(14)] + public void ExecuteNonQuery_NegativeTimeoutMS_ThrowsArgumentException() + { + var activity = new ExecuteNonQuery + { + ExistingDbConnection = new InArgument(_ => _fixture.Connection), + Sql = new InArgument("SELECT 1"), + TimeoutMS = new InArgument(-1), + AffectedRecords = new OutArgument() + }; + + Assert.Throws(() => WorkflowInvoker.Invoke(activity, TimeSpan.FromSeconds(30))); + } + private static string NewTempDbPath() => Path.Combine(Path.GetTempPath(), $"uipath_sqlite_txn_{Guid.NewGuid():N}.db"); @@ -343,13 +404,13 @@ private static DataTable BuildPeopleTable(params (int Id, string Name, int Age)[ } /// - /// Reads a argument at the end of a workflow body and exposes - /// it to the test, so the connection's post-scope state can be asserted. + /// Reads an argument at the end of a workflow body and exposes it to the test, so a + /// variable's post-scope state can be asserted. /// - private sealed class CaptureConnection : CodeActivity + private sealed class CaptureValue : CodeActivity { - public InArgument Input { get; set; } - public DatabaseConnection Value { get; private set; } + public InArgument Input { get; set; } + public T Value { get; private set; } protected override void Execute(CodeActivityContext context) {