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 support for parameterized queries #61

Merged
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
32 changes: 32 additions & 0 deletions InfluxData.Net.Common/Helpers/StringExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System;
using System.Text.RegularExpressions;

namespace InfluxData.Net.Common
{
public static class StringExtensions
{
// http://www.mvvm.ro/2011/03/sanitize-strings-against-sql-injection.html
public static string Sanitize(this string stringValue)
{
if (null == stringValue)
return stringValue;
return stringValue
.RegexReplace("-{2,}", "-")
.RegexReplace(@"[*/]+", string.Empty)
.RegexReplace(@"(;|\s)(exec|execute|select|insert|update|delete|create|alter|drop|rename|truncate|backup|restore)\s",
string.Empty, RegexOptions.IgnoreCase);
}


private static string RegexReplace(this string stringValue, string matchPattern, string toReplaceWith)
{
return Regex.Replace(stringValue, matchPattern, toReplaceWith);
}

private static string RegexReplace(this string stringValue, string matchPattern, string toReplaceWith, RegexOptions regexOptions)
{
return Regex.Replace(stringValue, matchPattern, toReplaceWith, regexOptions);
}

}
}
9 changes: 9 additions & 0 deletions InfluxData.Net.InfluxDb/ClientModules/BasicClientModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,22 @@
using InfluxData.Net.InfluxDb.RequestClients;
using InfluxData.Net.InfluxDb.ResponseParsers;
using InfluxData.Net.Common.Constants;
using InfluxData.Net.InfluxDb.Helpers;
using System;

namespace InfluxData.Net.InfluxDb.ClientModules
{
public class BasicClientModule : ClientModuleBase, IBasicClientModule
{
private readonly IBasicResponseParser _basicResponseParser;

public Task<IEnumerable<Serie>> QueryAsync(string query, object param = null, string dbName = null, string epochFormat = null, long? chunkSize = default(long?))
{
var buildQuery = QueryHelpers.BuildParameterizedQuery(query, param);

return this.QueryAsync(buildQuery, dbName, epochFormat, chunkSize);
}

public virtual async Task<IEnumerable<Serie>> QueryAsync(string query, string dbName = null, string epochFormat = null, long? chunkSize = null)
{
var series = await base.ResolveSingleGetSeriesResultAsync(query, dbName, epochFormat, chunkSize).ConfigureAwait(false);
Expand Down
12 changes: 12 additions & 0 deletions InfluxData.Net.InfluxDb/ClientModules/IBasicClientModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ namespace InfluxData.Net.InfluxDb.ClientModules
{
public interface IBasicClientModule
{
/// <summary>
/// Executes a parameterized query against the database. If chunkSize is specified, responses
/// will be broken down by number of returned rows.
/// </summary>
/// <param name="query">Query to execute.</param>
/// <param name="param">The parameters to pass, if any. (OPTIONAL)</param>
/// <param name="dbName">Database name. (OPTIONAL)</param>
/// <param name="epochFormat">Epoch timestamp format. (OPTIONAL)</param>
/// <param name="chunkSize">Maximum number of rows per chunk. (OPTIONAL)</param>
/// <returns></returns>
Task<IEnumerable<Serie>> QueryAsync(string query, object param = null, string dbName = null, string epochFormat = null, long? chunkSize = null);

/// <summary>
/// Executes a query against the database. If chunkSize is specified, responses
/// will be broken down by number of returned rows.
Expand Down
52 changes: 52 additions & 0 deletions InfluxData.Net.InfluxDb/Helpers/QueryHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System;
using System.Reflection;
using System.Text.RegularExpressions;
using InfluxData.Net.Common;
using System.Linq;

namespace InfluxData.Net.InfluxDb.Helpers
{
public static class QueryHelpers
{
public static string BuildParameterizedQuery(string query, object param)
{
Type t = param.GetType();
PropertyInfo[] pi = t.GetProperties();


foreach (var propertyInfo in pi)
{
var regex = $@"@{propertyInfo.Name}(?!\w)";

if(!Regex.IsMatch(query, regex) && Nullable.GetUnderlyingType(propertyInfo.GetType()) != null)
throw new ArgumentException($"Missing parameter identifier for @{propertyInfo.Name}");

var paramValue = propertyInfo.GetValue(param);
if (paramValue == null)
continue;

var paramType = paramValue.GetType();

if (!paramType.IsPrimitive && paramType != typeof(String) && paramType != typeof(DateTime))
throw new NotSupportedException($"The type {paramType.Name} is not a supported query parameter type.");

var sanitizedParamValue = paramValue;

if (paramType == typeof(String))
{
sanitizedParamValue = ((string)sanitizedParamValue).Sanitize();
}

while (Regex.IsMatch(query, regex))
{
var match = Regex.Match(query, regex);

query = query.Remove(match.Index, match.Length);
query = query.Insert(match.Index, $"{sanitizedParamValue}");
}
}

return query;
}
}
}
3 changes: 3 additions & 0 deletions InfluxData.Net.Tests/InfluxData.Net.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,7 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<Folder Include="InfluxDb\Helpers\" />
</ItemGroup>
</Project>
92 changes: 92 additions & 0 deletions InfluxData.Net.Tests/InfluxDb/Helpers/QueryHelpersTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using InfluxData.Net.InfluxDb.Helpers;
using Xunit;

namespace InfluxData.Net.Tests
{
[Trait("InfluxDb SerieExtensions", "Serie extensions")]
public class QueryHelpersTests
{
[Fact]
public void Building_Parameterized_Query_Returns_Correct_String()
{
var firstTag = "firstTag";
var firstTagValue = "firstTagValue";

var firstField = "firstField";
var firstFieldValue = "firstFieldValue";

var query = "SELECT * FROM fakeMeasurement " +
$"WHERE {firstTag} = @FirstTagValue " +
$"AND {firstField} = @FirstFieldValue";

var expectedNewQuery = "SELECT * FROM fakeMeasurement " +
$"WHERE {firstTag} = {firstTagValue} " +
$"AND {firstField} = {firstFieldValue}";

var actualNewQuery = QueryHelpers.BuildParameterizedQuery(
query,
new
{
@FirstTagValue = firstTagValue,
@FirstFieldValue = firstFieldValue
});

Assert.Equal(expectedNewQuery, actualNewQuery);
}


[Fact]
public void Using_Non_Primitive_And_Non_String_Type_In_Parameters_Throws_NotSupportedException()
{
var firstTag = "firstTag";
var firstTagValue = "firstTagValue";

var firstField = "firstField";

var query = "SELECT * FROM fakeMeasurement " +
$"WHERE {firstTag} = @FirstTagValue " +
$"AND {firstField} = @FirstFieldValue";

Func<string> func = new Func<string>(() =>
{
return QueryHelpers.BuildParameterizedQuery(
query,
new
{
@FirstTagValue = firstTagValue,
@FirstFieldValue = new List<string>() { "NOT ACCEPTED" }
});
});

Assert.Throws(typeof(NotSupportedException), func);
}

[Fact]
public void Building_Parameterized_Query_With_Missing_Parameters_Throws_ArgumentException()
{
var firstTag = "firstTag";
var firstTagValue = "firstTagValue";

var firstField = "firstField";

var query = "SELECT * FROM fakeMeasurement " +
$"WHERE {firstTag} = @FirstTagValue " +
$"AND {firstField} = @FirstFieldValue";

Func<string> func = new Func<string>(() =>
{
return QueryHelpers.BuildParameterizedQuery(
query,
new
{
@FirstTagValue = firstTagValue
});
});

Assert.Throws(typeof(ArgumentException), func);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,38 @@ public virtual async Task ClientQuery_OnExistingPoints_ShouldReturnSerieCollecti
result.First().Values.Should().HaveCount(3);
}

[Fact]
public virtual async Task ClientQuery_Parameterized_OnExistingPoints_ShouldReturnSerieCollection()
{
var points = await _fixture.MockAndWritePoints(3);

var firstTag = points.First().Tags.First().Key;
var firstTagValue = points.First().Tags.First().Value;

var firstField = points.First().Fields.First().Key;
var firstFieldValue = points.First().Fields.First().Value;

var query = $"SELECT * FROM {points.First().Name} " +
$@"WHERE {firstTag} = '@FirstTagValueParam' " +
$@"AND {firstField} = @FirstFieldValueParam";


var result = await _fixture.Sut.Client.QueryAsync(
query,
new
{
@FirstTagValueParam = firstTagValue,
@FirstFieldValueParam = firstFieldValue
}, _fixture.DbName);

var t = result.First();

result.Should().NotBeNull();
result.Should().HaveCount(1);
result.First().Name.Should().Be(points.First().Name);
result.First().Values.Should().HaveCount(1);
}

[Fact]
public virtual async Task ClientQueryMultiple_OnExistingPoints_ShouldReturnSerieCollection()
{
Expand Down