diff --git a/Milyli.ScriptRunner.Core.Test/UnitTests/RelativityScriptProcessorTests.cs b/Milyli.ScriptRunner.Core.Test/UnitTests/RelativityScriptProcessorTests.cs index 2a9ea7f..569c5d3 100644 --- a/Milyli.ScriptRunner.Core.Test/UnitTests/RelativityScriptProcessorTests.cs +++ b/Milyli.ScriptRunner.Core.Test/UnitTests/RelativityScriptProcessorTests.cs @@ -170,6 +170,50 @@ public void ReplaceEddsPrepend_Admin() Assert.AreEqual(expectedSql, generatedSql); } + [TestCase(RelativityScriptInputDetailsScriptInputType.Field)] + [TestCase(RelativityScriptInputDetailsScriptInputType.Sql)] + [TestCase(RelativityScriptInputDetailsScriptInputType.SearchProvider)] + [TestCase(RelativityScriptInputDetailsScriptInputType.Constant)] + [Description("Verifies that when optional inputs are not specified NULL is inserted.")] + public void ReplaceScriptInput_OptionalNotSpecified(RelativityScriptInputDetailsScriptInputType inputType) + { + // Arrange + var templateSql = @"UPDATE [Foo] +SET[Bar] = {0}"; + var inputId = "optional_input"; + var otherInputId = "input_unused"; + var expectedSql = string.Format(templateSql, "NULL"); + var scriptSql = string.Format(templateSql, $"#{inputId}#"); + + var populatedInputs = new List + { + new JobScriptInput + { + InputId = otherInputId, + InputValue = "unused doesn't matter", + } + }; + var relativityInputs = new List + { + new RelativityScriptInputDetails + { + Id = inputId, + InputType = inputType, + }, + new RelativityScriptInputDetails + { + Id = otherInputId, + InputType = RelativityScriptInputDetailsScriptInputType.Sql, + } + }; + + // Act + var generatedSql = this.relativityScriptProcessor.SubstituteScriptInputs(populatedInputs, relativityInputs, scriptSql); + + // Assert + Assert.AreEqual(expectedSql, generatedSql); + } + [TestCase(RelativityScriptInputDetailsScriptInputType.Field)] [TestCase(RelativityScriptInputDetailsScriptInputType.Sql)] [TestCase(RelativityScriptInputDetailsScriptInputType.SearchProvider)] diff --git a/Milyli.ScriptRunner.Core/Tools/RelativityScriptProcessor.cs b/Milyli.ScriptRunner.Core/Tools/RelativityScriptProcessor.cs index d482c51..f76fbed 100644 --- a/Milyli.ScriptRunner.Core/Tools/RelativityScriptProcessor.cs +++ b/Milyli.ScriptRunner.Core/Tools/RelativityScriptProcessor.cs @@ -84,6 +84,10 @@ public string SubstituteGlobalVariables(int workspaceId, string inputSql) /// public string SubstituteSavedSearchTables(IEnumerable populatedInputs, IEnumerable relativityScriptInputDetails, IDictionary searchTablePairs, string inputSql) { + // Optional input are not handled explicitly for saved searches -- + // Per relativity this is not officially supported, so if an optional search input + // is not supplied the script execution will simply fail. + // https://devhelp.relativity.com/t/documentation-around-non-required-relativity-script-inputs/6535/5 var mappedInputs = populatedInputs.Join( relativityScriptInputDetails, p => p.InputId, @@ -121,17 +125,25 @@ public string SubstituteScriptInputs( IEnumerable relativityScriptInputDetails, string inputSql) { - var mappedInputs = populatedInputs.Join( - relativityScriptInputDetails, - p => p.InputId, - d => d.Id, - (p, d) => new + // The group join essentially allows a left join of the two lists -- + // If an input is optional and not used, it will simply be null + var mappedInputs = relativityScriptInputDetails + .GroupJoin( + populatedInputs, + details => details.Id, + populated => populated.InputId, + (details, populated) => new + { + details, + populated = populated.DefaultIfEmpty() + }) + .SelectMany(joined => joined.populated.Select(input => new { - p.InputValue, - d.Attributes, - d.Id, - d.InputType - }); + input?.InputValue, + joined.details.Id, + joined.details.InputType, + joined.details.Attributes, + })); foreach (var populatedInput in mappedInputs) { @@ -139,34 +151,44 @@ public string SubstituteScriptInputs( // By default do not modify the token, e.g. to later process saved searches var replaceString = replaceToken; - switch (populatedInput.InputType) + + // InputValues should only be null on optional inputs which were not supplied + // In this case a literal SQL NULL is inserted. + if (populatedInput.InputValue == null) { - // Field, Sql, Object, Search provider input types are all simply direct substitution - // In the case of SQL inputs ScriptRunner has already saved the generated value. - case RelativityScriptInputDetailsScriptInputType.Field: - case RelativityScriptInputDetailsScriptInputType.Sql: - case RelativityScriptInputDetailsScriptInputType.SearchProvider: - replaceString = populatedInput.InputValue; - break; - case RelativityScriptInputDetailsScriptInputType.Constant: - // Constant inputs are typed as strings or directly substituted depending on the underying type - var containsDataType = populatedInput.Attributes.ContainsKey("DataType"); - if (containsDataType && textDataTypes.Contains(populatedInput.Attributes["DataType"])) - { - replaceString = $"'{populatedInput.InputValue}'"; - } - else if (containsDataType && populatedInput.Attributes["DataType"] == "timezone") - { - var timeZoneName = populatedInput.InputValue; - var hoursToUse = TimeZoneInfo.GetSystemTimeZones().FirstOrDefault(tzi => tzi.Id == timeZoneName).GetUtcOffset(DateTime.UtcNow).TotalHours; - replaceString = string.Format("{0:0.##}", hoursToUse); - } - else - { + replaceString = "NULL"; + } + else + { + switch (populatedInput.InputType) + { + // Field, Sql, Object, Search provider input types are all simply direct substitution + // In the case of SQL inputs ScriptRunner has already saved the generated value. + case RelativityScriptInputDetailsScriptInputType.Field: + case RelativityScriptInputDetailsScriptInputType.Sql: + case RelativityScriptInputDetailsScriptInputType.SearchProvider: replaceString = populatedInput.InputValue; - } - - break; + break; + case RelativityScriptInputDetailsScriptInputType.Constant: + // Constant inputs are typed as strings or directly substituted depending on the underying type + var containsDataType = populatedInput.Attributes.ContainsKey("DataType"); + if (containsDataType && textDataTypes.Contains(populatedInput.Attributes["DataType"])) + { + replaceString = $"'{populatedInput.InputValue}'"; + } + else if (containsDataType && populatedInput.Attributes["DataType"] == "timezone") + { + var timeZoneName = populatedInput.InputValue; + var hoursToUse = TimeZoneInfo.GetSystemTimeZones().FirstOrDefault(tzi => tzi.Id == timeZoneName).GetUtcOffset(DateTime.UtcNow).TotalHours; + replaceString = string.Format("{0:0.##}", hoursToUse); + } + else + { + replaceString = populatedInput.InputValue; + } + + break; + } } inputSql = Regex.Replace(