Skip to content

Commit

Permalink
feat: support some linq string functions in where conditions, fixes i…
Browse files Browse the repository at this point in the history
  • Loading branch information
jogibear9988 committed Jul 13, 2023
1 parent 44574e0 commit 971245d
Show file tree
Hide file tree
Showing 4 changed files with 212 additions and 0 deletions.
42 changes: 42 additions & 0 deletions Client.Linq.Test/InfluxDBQueryVisitorTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,48 @@ public void InitQueryApi()
_queryApi = new Mock<QueryApiSync>(options, queryService.Object, new FluxResultMapper()).Object;
}

[Test]
public void StringFunctionsQuery()
{
var query = from s in InfluxDBQueryable<Sensor>.Queryable("my-bucket", "my-org", _queryApi)
where s.SensorId.ToLower().Contains("aaa")
select s;
var visitor = BuildQueryVisitor(query);

const string expected =
"import \"strings\"\nstart_shifted = int(v: time(v: p2))\n\nfrom(bucket: p1) |> range(start: time(v: start_shifted)) |> filter(fn: (r) => strings.containsStr(v: strings.toLower(v: r[\"sensor_id\"]), substr: p3)) |> pivot(rowKey:[\"_time\"], columnKey: [\"_field\"], valueColumn: \"_value\") |> drop(columns: [\"_start\", \"_stop\", \"_measurement\"]) |> filter(fn: (r) => strings.containsStr(v: strings.toLower(v: r[\"sensor_id\"]), substr: p3))";
var qry = visitor.BuildFluxQuery();
Assert.AreEqual(expected, visitor.BuildFluxQuery());
}

[Test]
public void ToStringFunctionQuery()
{
var query = from s in InfluxDBQueryable<Sensor>.Queryable("my-bucket", "my-org", _queryApi)
where s.Value.ToString() == "3"
select s;
var visitor = BuildQueryVisitor(query);

const string expected =
"start_shifted = int(v: time(v: p2))\n\nfrom(bucket: p1) |> range(start: time(v: start_shifted)) |> filter(fn: (r) => (string(v: r[\"data\"]) == p3)) |> pivot(rowKey:[\"_time\"], columnKey: [\"_field\"], valueColumn: \"_value\") |> drop(columns: [\"_start\", \"_stop\", \"_measurement\"]) |> filter(fn: (r) => (string(v: r[\"data\"]) == p3))";
var qry = visitor.BuildFluxQuery();
Assert.AreEqual(expected, visitor.BuildFluxQuery());
}

[Test]
public void ReplaceAllFunctionQuery()
{
var query = from s in InfluxDBQueryable<Sensor>.Queryable("my-bucket", "my-org", _queryApi)
where s.SensorId.ToLower().Replace("a", "b") == "b"
select s;
var visitor = BuildQueryVisitor(query);

const string expected =
"import \"strings\"\nstart_shifted = int(v: time(v: p2))\n\nfrom(bucket: p1) |> range(start: time(v: start_shifted)) |> filter(fn: (r) => (strings.replaceAll(v: strings.toLower(v: r[\"sensor_id\"]), t: p3, u: p4) == p5)) |> pivot(rowKey:[\"_time\"], columnKey: [\"_field\"], valueColumn: \"_value\") |> drop(columns: [\"_start\", \"_stop\", \"_measurement\"]) |> filter(fn: (r) => (strings.replaceAll(v: strings.toLower(v: r[\"sensor_id\"]), t: p3, u: p4) == p5))";
var qry = visitor.BuildFluxQuery();
Assert.AreEqual(expected, visitor.BuildFluxQuery());
}

[Test]
public void DefaultQuery()
{
Expand Down
58 changes: 58 additions & 0 deletions Client.Linq/Internal/Expressions/StringFunction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System.Collections.Generic;
using System.Text;

namespace InfluxDB.Client.Linq.Internal.Expressions
{
internal class StringFunction : IExpressionPart
{
private readonly string functionName;
private readonly IEnumerable<IExpressionPart> expressionParts;
private readonly IEnumerable<IExpressionPart> expressionPartsPar2;
private readonly IEnumerable<IExpressionPart> expressionPartsPar3;

internal StringFunction(string functionName, IEnumerable<IExpressionPart> expressionParts,
IEnumerable<IExpressionPart> expressionPartsPar2, IEnumerable<IExpressionPart> expressionPartsPar3)
{
this.functionName = functionName;
this.expressionParts = expressionParts;
this.expressionPartsPar2 = expressionPartsPar2;
this.expressionPartsPar3 = expressionPartsPar3;
}

public void AppendFlux(StringBuilder builder)
{
if (functionName != "string")
{
builder.Append("strings.");
}

builder.Append(functionName);
builder.Append("(v: ");
foreach (var expressionPart in expressionParts) expressionPart.AppendFlux(builder);
if (functionName == "containsStr")
{
builder.Append(", substr: ");
foreach (var expressionPart in expressionPartsPar2) expressionPart.AppendFlux(builder);
}
else if (functionName == "hasPrefix")
{
builder.Append(", prefix: ");
foreach (var expressionPart in expressionPartsPar2) expressionPart.AppendFlux(builder);
}
else if (functionName == "hasSuffix")
{
builder.Append(", suffix: ");
foreach (var expressionPart in expressionPartsPar2) expressionPart.AppendFlux(builder);
}
else if (functionName == "replaceAll")
{
builder.Append(", t: ");
foreach (var expressionPart in expressionPartsPar2) expressionPart.AppendFlux(builder);
builder.Append(", u: ");
foreach (var expressionPart in expressionPartsPar3) expressionPart.AppendFlux(builder);
}

builder.Append(")");
}
}
}
23 changes: 23 additions & 0 deletions Client.Linq/Internal/QueryAggregator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ internal class QueryAggregator
private ResultFunction _resultFunction;
private readonly List<string> _filterByTags;
private readonly List<string> _filterByFields;
private HashSet<string> _imports;
private readonly List<(string, string, bool, string)> _orders;
private (string Every, string Period, string Fn)? _aggregateWindow;

Expand All @@ -74,6 +75,7 @@ internal QueryAggregator()
_limitTailNOffsetAssignments = new List<LimitOffsetAssignment>();
_filterByTags = new List<string>();
_filterByFields = new List<string>();
_imports = null;
_orders = new List<(string, string, bool, string)>();
_aggregateWindow = null;
}
Expand All @@ -83,6 +85,16 @@ internal void AddBucket(string bucket)
_bucketAssignment = bucket;
}

internal void AddImport(string import)
{
if (_imports == null)
{
_imports = new HashSet<string>();
}

_imports.Add(import);
}

internal void AddRangeStart(string rangeStart, RangeExpressionType expressionType)
{
_rangeStartAssignment = rangeStart;
Expand Down Expand Up @@ -219,6 +231,7 @@ internal string BuildFluxQuery(QueryableOptimizerSettings settings)

var query = new StringBuilder();

query.Append(BuildImports());
query.Append(JoinList(transforms, "\n"));
query.Append("\n\n");
query.Append(JoinList(parts, " |> "));
Expand Down Expand Up @@ -334,6 +347,16 @@ private string BuildFilter(IEnumerable<string> filterBy)
return filter.ToString();
}

private string BuildImports()
{
if (_imports != null)
{
return string.Join("", _imports.Select(x => "import \"" + x + "\"\n"));
}

return string.Empty;
}

private string BuildOperator(string operatorName, params object[] variables)
{
var builderVariables = new StringBuilder();
Expand Down
89 changes: 89 additions & 0 deletions Client.Linq/Internal/QueryExpressionTreeVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,88 @@ protected override Expression VisitMethodCall(MethodCallExpression expression)
return expression;
}

if (expression.Method.DeclaringType == typeof(string))
{
if (expression.Method.Name.Equals("ToLower"))
{
_context.QueryAggregator.AddImport("strings");
var partsCount = _expressionParts.Count;
Visit(expression.Object);
var part = GetAndRemoveExpressionParts(partsCount, _expressionParts.Count);
_expressionParts.Add(new StringFunction("toLower", part, null, null));
return expression;
}
else if (expression.Method.Name.Equals("ToUpper"))
{
_context.QueryAggregator.AddImport("strings");
var partsCount = _expressionParts.Count;
Visit(expression.Object);
var part = GetAndRemoveExpressionParts(partsCount, _expressionParts.Count);
_expressionParts.Add(new StringFunction("toUpper", part, null, null));
return expression;
}
else if (expression.Method.Name.Equals("Contains"))
{
_context.QueryAggregator.AddImport("strings");
var partsCount = _expressionParts.Count;
Visit(expression.Object);
var part = GetAndRemoveExpressionParts(partsCount, _expressionParts.Count);
partsCount = _expressionParts.Count;
Visit(expression.Arguments[0]);
var part2 = GetAndRemoveExpressionParts(partsCount, _expressionParts.Count);
_expressionParts.Add(new StringFunction("containsStr", part, part2, null));
return expression;
}
else if (expression.Method.Name.Equals("StartsWith"))
{
_context.QueryAggregator.AddImport("strings");
var partsCount = _expressionParts.Count;
Visit(expression.Object);
var part = GetAndRemoveExpressionParts(partsCount, _expressionParts.Count);
partsCount = _expressionParts.Count;
Visit(expression.Arguments[0]);
var part2 = GetAndRemoveExpressionParts(partsCount, _expressionParts.Count);
_expressionParts.Add(new StringFunction("hasPrefix", part, part2, null));
return expression;
}
else if (expression.Method.Name.Equals("EndsWith"))
{
_context.QueryAggregator.AddImport("strings");
var partsCount = _expressionParts.Count;
Visit(expression.Object);
var part = GetAndRemoveExpressionParts(partsCount, _expressionParts.Count);
partsCount = _expressionParts.Count;
Visit(expression.Arguments[0]);
var part2 = GetAndRemoveExpressionParts(partsCount, _expressionParts.Count);
_expressionParts.Add(new StringFunction("hasSuffix", part, part2, null));
return expression;
}
else if (expression.Method.Name.Equals("Replace"))
{
_context.QueryAggregator.AddImport("strings");
var partsCount = _expressionParts.Count;
Visit(expression.Object);
var part = GetAndRemoveExpressionParts(partsCount, _expressionParts.Count);
partsCount = _expressionParts.Count;
Visit(expression.Arguments[0]);
var part2 = GetAndRemoveExpressionParts(partsCount, _expressionParts.Count);
partsCount = _expressionParts.Count;
Visit(expression.Arguments[1]);
var part3 = GetAndRemoveExpressionParts(partsCount, _expressionParts.Count);
_expressionParts.Add(new StringFunction("replaceAll", part, part2, part3));
return expression;
}
}

if (expression.Method.Name.Equals("ToString"))
{
var partsCount = _expressionParts.Count;
Visit(expression.Object);
var part = GetAndRemoveExpressionParts(partsCount, _expressionParts.Count);
_expressionParts.Add(new StringFunction("string", part, null, null));
return expression;
}

return base.VisitMethodCall(expression);
}

Expand Down Expand Up @@ -294,6 +376,13 @@ private void NormalizeNamedFieldValue()
NormalizeNamedFieldValue();
}

private IEnumerable<IExpressionPart> GetAndRemoveExpressionParts(int start, int end)
{
var parts = _expressionParts.GetRange(start, end - start);
for (var i = start; i < end; i++) _expressionParts.RemoveAt(i);
return parts;
}

/// <summary>
/// Mark variables that are use to filter by tag by tag as tag.
/// </summary>
Expand Down

0 comments on commit 971245d

Please sign in to comment.