Skip to content

Commit

Permalink
feat: support some linq string functions in where conditions, fixes #499
Browse files Browse the repository at this point in the history
  • Loading branch information
jogibear9988 committed Apr 3, 2023
1 parent 30c1b61 commit 817a9f0
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 0 deletions.
36 changes: 36 additions & 0 deletions Client.Linq.Test/InfluxDBQueryVisitorTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,42 @@ 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(")");
}
}
}
19 changes: 19 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,15 @@ 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 +230,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 +346,13 @@ 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
88 changes: 88 additions & 0 deletions Client.Linq/Internal/QueryExpressionTreeVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,84 @@ 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 = this._expressionParts.Count;
this.Visit(expression.Object);
var part = this.GetAndRemoveExpressionParts(partsCount, this._expressionParts.Count);
this._expressionParts.Add(new StringFunction("toLower", part, null, null));
return expression;
}
else if (expression.Method.Name.Equals("ToUpper"))
{
_context.QueryAggregator.AddImport("strings");
var partsCount = this._expressionParts.Count;
this.Visit(expression.Object);
var part = this.GetAndRemoveExpressionParts(partsCount, this._expressionParts.Count);
this._expressionParts.Add(new StringFunction("toUpper", part, null, null));
return expression;
}
else if (expression.Method.Name.Equals("Contains"))
{
_context.QueryAggregator.AddImport("strings");
var partsCount = this._expressionParts.Count;
this.Visit(expression.Object);
var part = this.GetAndRemoveExpressionParts(partsCount, this._expressionParts.Count);
partsCount = this._expressionParts.Count;
this.Visit(expression.Arguments[0]);
var part2 = this.GetAndRemoveExpressionParts(partsCount, this._expressionParts.Count);
this._expressionParts.Add(new StringFunction("containsStr", part, part2, null));
return expression;
}
else if (expression.Method.Name.Equals("StartsWith")) {
_context.QueryAggregator.AddImport("strings");
var partsCount = this._expressionParts.Count;
this.Visit(expression.Object);
var part = this.GetAndRemoveExpressionParts(partsCount, this._expressionParts.Count);
partsCount = this._expressionParts.Count;
this.Visit(expression.Arguments[0]);
var part2 = this.GetAndRemoveExpressionParts(partsCount, this._expressionParts.Count);
this._expressionParts.Add(new StringFunction("hasPrefix", part, part2, null));
return expression;
}
else if (expression.Method.Name.Equals("EndsWith")) {
_context.QueryAggregator.AddImport("strings");
var partsCount = this._expressionParts.Count;
this.Visit(expression.Object);
var part = this.GetAndRemoveExpressionParts(partsCount, this._expressionParts.Count);
partsCount = this._expressionParts.Count;
this.Visit(expression.Arguments[0]);
var part2 = this.GetAndRemoveExpressionParts(partsCount, this._expressionParts.Count);
this._expressionParts.Add(new StringFunction("hasSuffix", part, part2, null));
return expression;
}
else if (expression.Method.Name.Equals("Replace")) {
_context.QueryAggregator.AddImport("strings");
var partsCount = this._expressionParts.Count;
this.Visit(expression.Object);
var part = this.GetAndRemoveExpressionParts(partsCount, this._expressionParts.Count);
partsCount = this._expressionParts.Count;
this.Visit(expression.Arguments[0]);
var part2 = this.GetAndRemoveExpressionParts(partsCount, this._expressionParts.Count);
partsCount = this._expressionParts.Count;
this.Visit(expression.Arguments[1]);
var part3 = this.GetAndRemoveExpressionParts(partsCount, this._expressionParts.Count);
this._expressionParts.Add(new StringFunction("replaceAll", part, part2, part3));
return expression;
}
}

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

return base.VisitMethodCall(expression);
}

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

private IEnumerable<IExpressionPart> GetAndRemoveExpressionParts(int start, int end)
{
var parts = this._expressionParts.GetRange(start, end - start);
for (int i = start; i < end; i++)
{
this._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 817a9f0

Please sign in to comment.