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

Substitute null for unspecified optional inputs in direct SQL execution #31

Merged
merged 1 commit into from
Jun 21, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<JobScriptInput>
{
new JobScriptInput
{
InputId = otherInputId,
InputValue = "unused doesn't matter",
}
};
var relativityInputs = new List<RelativityScriptInputDetails>
{
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)]
Expand Down
94 changes: 58 additions & 36 deletions Milyli.ScriptRunner.Core/Tools/RelativityScriptProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ public string SubstituteGlobalVariables(int workspaceId, string inputSql)
/// <inheritdoc />
public string SubstituteSavedSearchTables(IEnumerable<JobScriptInput> populatedInputs, IEnumerable<RelativityScriptInputDetails> relativityScriptInputDetails, IDictionary<int, string> 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
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a way we can hard fail here with a clear error message? Or is that actually worse than failing like Relativity would?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'd rather just let it fall through to the error you'd get with the script since the default behavior is "it's just null, even if it's really an unescaped string substituted for a column name" (not sure how that works, but it does), except in the case of saved searches where it just flat out doesn't work anyway.

var mappedInputs = populatedInputs.Join(
relativityScriptInputDetails,
p => p.InputId,
Expand Down Expand Up @@ -121,52 +125,70 @@ public string SubstituteScriptInputs(
IEnumerable<RelativityScriptInputDetails> 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)
{
var replaceToken = $"#{populatedInput.Id}#";

// 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(
Expand Down